├── Images ├── ClarifaiApp1.png ├── ClarifaiApp2.png ├── ClarifaiTagger1.png ├── ClarifaiTagger2.png ├── PluginManager1.png ├── PluginManager2.png └── ClarifaiTagger_Menu.png ├── .gitignore ├── ClarifaiTagger.lrdevplugin ├── Info.lua ├── ClarifaiTaggerInit.lua ├── ClarifaiAPI.lua ├── LUTILS.lua ├── KwUtils.lua ├── TaggerDialog.lua ├── ClarifaiTaggerInfoProvider.lua └── JSON.lua ├── LICENSE └── README.md /Images/ClarifaiApp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safx/LightroomPlugin-ClarifaiTagger/HEAD/Images/ClarifaiApp1.png -------------------------------------------------------------------------------- /Images/ClarifaiApp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safx/LightroomPlugin-ClarifaiTagger/HEAD/Images/ClarifaiApp2.png -------------------------------------------------------------------------------- /Images/ClarifaiTagger1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safx/LightroomPlugin-ClarifaiTagger/HEAD/Images/ClarifaiTagger1.png -------------------------------------------------------------------------------- /Images/ClarifaiTagger2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safx/LightroomPlugin-ClarifaiTagger/HEAD/Images/ClarifaiTagger2.png -------------------------------------------------------------------------------- /Images/PluginManager1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safx/LightroomPlugin-ClarifaiTagger/HEAD/Images/PluginManager1.png -------------------------------------------------------------------------------- /Images/PluginManager2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safx/LightroomPlugin-ClarifaiTagger/HEAD/Images/PluginManager2.png -------------------------------------------------------------------------------- /Images/ClarifaiTagger_Menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safx/LightroomPlugin-ClarifaiTagger/HEAD/Images/ClarifaiTagger_Menu.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .AppleDouble 3 | .LSOverride 4 | 5 | # debug logs 6 | *.log 7 | *.log.txt 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | .com.apple.timemachine.donotpresent 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | # Compiled Lightroom plugins 32 | *.lrplugin 33 | 34 | # Textmate project files: 35 | *.tmproj 36 | *.tmproject 37 | tmtags 38 | -------------------------------------------------------------------------------- /ClarifaiTagger.lrdevplugin/Info.lua: -------------------------------------------------------------------------------- 1 | local menuItems = { 2 | title = LOC "$$$/ClarifaiTagger/OpenClarifaiTagger=Request keyword suggestions from Clarifai", 3 | file = "TaggerDialog.lua", 4 | enabledWhen = "photosSelected" 5 | } 6 | 7 | return { 8 | LrSdkVersion = 5.0, 9 | LrSdkMinimumVersion = 5.0, 10 | 11 | LrToolkitIdentifier = "com.blogspot.safxdev.tagger.clarifai", 12 | LrPluginInfoUrl = "https://github.com/safx/LightroomPlugin-ClarifaiTagger", 13 | 14 | LrPluginName = "ClarifaiTagger", 15 | -- Put in both "File" and "Library" plugin menus 16 | LrExportMenuItems = menuItems, 17 | LrLibraryMenuItems = menuItems, 18 | LrPluginInfoProvider = 'ClarifaiTaggerInfoProvider.lua', 19 | LrInitPlugin = 'ClarifaiTaggerInit.lua', 20 | 21 | VERSION = {display='1.1.0', major=1, minor=1, revision=0, build=20161011}, 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /ClarifaiTagger.lrdevplugin/ClarifaiTaggerInit.lua: -------------------------------------------------------------------------------- 1 | -- Provide initial default values for plugin preferences. 2 | 3 | local prefs = import 'LrPrefs'.prefsForPlugin(_PLUGIN.id) 4 | 5 | local defaultPrefValues = { 6 | -- CLARIFAI CONFIGURATION 7 | clientId = 'Copy from application on Clarifai.com', 8 | clientSecret = 'Copy from application on Clarifai.com', 9 | imageSize = 600, -- Default for size of image sent to Clarifai 10 | keywordLanguage = '', -- Default is language used for creating your Clarifai application 11 | 12 | -- TAGGING WINDOW SETTINGS 13 | thumbnailSize = 300, -- Thumbnails shown above columns of keyword checkboxes 14 | -- Show tags that already are in the Lightroom catalog keyword list in bold 15 | boldExistingKeywords = true, 16 | -- Automatically select keywords that already exist in the Lr Catalog keyword list 17 | autoSelectExistingKeywords = true, 18 | -- Only auto-select keyword suggestions if the probability is above this value 19 | autoSelectProbabilityThreshold = 85, 20 | -- Display the Clarifai-assessed probability for each keyword 21 | showProbability = false, 22 | -- Dimensions of the tagging window (default is low and matches former hard-coded setting, but can be set very high) 23 | taggingWindowHeight = 680, -- pixels high 24 | taggingWindowWidth = 880, -- pixels wide 25 | 26 | -- Dimensions of the tagging window (default is low to avoid being larger than available screen space) 27 | imagePreviewWindowHeight = 800, 28 | imagePreviewWindowWidth = 1250, 29 | 30 | -- For hierarchical keyword lists, ignore branches in your keyword tree which include 31 | -- terms which Clarifai will never return. e.g. branches with species names, custom process tags, etc. 32 | -- Skipping branches with many such terms can greatly boost performance since this plugin 33 | -- scans all keywords in your Lightroom catalog to match terms returned by Clarifai with 34 | -- existing keywords in your system. 35 | ignore_keyword_branches = '', 36 | } 37 | 38 | for k,v in pairs(defaultPrefValues) do 39 | if prefs[k] == nil then prefs[k] = v end 40 | end 41 | -------------------------------------------------------------------------------- /ClarifaiTagger.lrdevplugin/ClarifaiAPI.lua: -------------------------------------------------------------------------------- 1 | local LrHttp = import 'LrHttp' 2 | local LrLogger = import 'LrLogger' 3 | local LrPathUtils = import 'LrPathUtils' 4 | local LrStringUtils = import 'LrStringUtils' 5 | local LrFileUtils = import 'LrFileUtils' 6 | local JSON = require 'JSON' 7 | local prefs = import 'LrPrefs'.prefsForPlugin(_PLUGIN.id) 8 | 9 | local logger = LrLogger('ClarifaiAPI') 10 | logger:enable('print') 11 | 12 | 13 | local tagAPIURL = 'https://api.clarifai.com/v2/models/aaa03c23b3724a16a56b629203edc62c/outputs' 14 | 15 | -------------------------------- 16 | 17 | ClarifaiAPI = {} 18 | 19 | function ClarifaiAPI.getTags_impl(photos, thumbnailPaths) 20 | local headers = { 21 | { field = 'Authorization', value = 'Key ' .. prefs.clientId }, 22 | -- { field = 'Authorization', value = 'Key e2a415c0928445c3864ac960713e9dee' }, 23 | { field = 'Content-Type', value = 'application/json' }, 24 | -- { field = 'Accept-Language', value = prefs.keywordLanguage }, 25 | }; 26 | 27 | local payload_prefix = '{"inputs": [' 28 | local payload_middle = '' 29 | local payload_postfix = ']}' 30 | for idx, photo in ipairs(photos) do 31 | --payload_middle = '{"data":{"image":{"url": "https://samples.clarifai.com/metro-north.jpg"}}}'; 32 | payload_middle = payload_middle .. '{"data":{"image":{"base64": "' .. LrStringUtils.encodeBase64(LrFileUtils.readFile(thumbnailPaths[idx])) .. '"}}},'; 33 | end 34 | payload_middle = payload_middle:sub(1, -2) 35 | local payload = payload_prefix .. payload_middle .. payload_postfix; 36 | logger:info(' get tags START'); 37 | local body, reshdrs = LrHttp.post(tagAPIURL, payload, headers, "POST", 50, string.len(payload)) 38 | -- logger:info(' get tags body: ', body); 39 | 40 | local json = JSON:decode(body); 41 | return json, reshdrs.status; 42 | end 43 | 44 | function ClarifaiAPI.getTags(photos, thumbnailPaths) 45 | -- if prefs.accessToken == nil then 46 | -- ClarifaiAPI.getToken(); 47 | -- end 48 | 49 | local json, status = ClarifaiAPI.getTags_impl(photos, thumbnailPaths); 50 | -- if status == 401 then 51 | -- ClarifaiAPI.getToken(); 52 | -- json, status = ClarifaiAPI.getTags_impl(photos, thumbnailPaths); 53 | -- end 54 | 55 | return json 56 | end 57 | 58 | 59 | return ClarifaiAPI 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LightroomPlugin-ClarifaiTagger 2 | 3 | ![](Images/ClarifaiTagger1.png) 4 | 5 | This Lightroom plugin helps you to add keywords to your photos, powered by the [Clarifai](http://www.clarifai.com/), visual recognition service. 6 | 7 | * works with JPEG and Raw files 8 | * works on Windows and Mac OS X (not yet tested on Windows) 9 | 10 | ## Create a Developer Account on Clarifai.com 11 | 12 | To use ClarifaiTagger, you must first create a developer account on [Clarifai](http://www.clarifai.com/) and create an application. 13 | 14 | 1. Go to [Clarifai](http://www.clarifai.com/) and create a developer accout. 15 | 16 | 1. Click "Create an application" from Applications → Create a new Application. 17 | 18 | ![](Images/ClarifaiApp1.png) 19 | 20 | 1. Once you create your application, the Client ID and Client Secret are provided. 21 | 22 | ![](Images/ClarifaiApp2.png) 23 | 24 | ## Installation & Setup 25 | 26 | To install ClarifaiTagger, follow these steps: 27 | 28 | 1. Clone or download this project. 29 | 30 | 1. Open "Lightroom Plug-in Manager" from Lightroom menu → File → Plug-in Manager... 31 | 32 | 1. Click "Add" and select the `ClarifaiTagger.lrdevplugin`. 33 | 34 | ![](Images/PluginManager1.png) 35 | 36 | Or, simply put the `ClarifaiTagger.lrdevplugin` in its standard location as follows: 37 | 38 | * Mac OS X (current user only): `~/Library/Application Support/Adobe/Lightroom/Modules` 39 | * Mac OS X (for all users): `/Library/Application Support/Adobe/Lightroom/Modules` 40 | * Windows: `C:\Users\username\AppData\Roaming\Adobe\Lightroom\Modules` 41 | 42 | 1. Fill the `Client ID` and `Client Secret` fields with the values provided by clarifai.com for the application you've created. 43 | 44 | ![](Images/PluginManager2.png) 45 | 46 | ## How to use 47 | 48 | 1. Select the photos for which you want to add keywords. You may select up to 128 photos (the maximum supported by Clarifai). 49 | 1. Choose `Request keyword suggestions from Clarifai` from Lightroom’s `Library → Plugin-Extras` menu. 50 | 51 | ![](Images/ClarifaiTagger_Menu.png) 52 | 53 | 1. After a few seconds, the Clarifai Tagger window should pop up with the keywords suggested by Clarifai for each selected image. 54 | 55 | ![](Images/ClarifaiTagger1.png) 56 | 57 | 1. Check keywords you want to add. 58 | 1. Click "Save" to apply changes, or "Cancel" otherwise. 59 | 60 | ## Preferences 61 | 62 | ![](Images/PluginManager2.png) 63 | 64 | ### Tagging 65 | 66 | * **Show Existing Keywords as Bold**: uses bold face for keywords which are already in the catalog's keyword list 67 | * **Automatically Select Existing Keywords**: automatically selects the checkboxes for keywords which are already in the catalog's keyword list 68 | * **Show Probability**: Display each keyword's rated probability 69 | ![](Images/ClarifaiTagger2.png) 70 | 71 | ### Image Settings 72 | 73 | * **Image Size**: sets the size of thumbnail images sent to Clarifai 74 | * **Keyword Language**: determines the language of keywords received from Clarifai. By default, it's the language you configured as default for your application (on clarifai.com), however you can change this setting in the plugin preferences to receive keywords in some other language, if you desire. 75 | -------------------------------------------------------------------------------- /ClarifaiTagger.lrdevplugin/LUTILS.lua: -------------------------------------------------------------------------------- 1 | --[[---------------------------------------------------------------------------- 2 | 3 | LUTILS.lua 4 | Utility functions for common Lua tasks. This is a bundle intended to provide 5 | utility functions. Since it's for use in Lightroom plugins, it uses Lua 5.1.x 6 | This bundle may grow over time, but is intended to remain limited in scope to 7 | avoid including this file from causing undue bloat. 8 | 9 | -------------------------------------------------------------------------------- 10 | 11 | Copyright 2016 Lowell ("LoweMo" / "LoMo") Montgomery 12 | https://lowemo.photo 13 | Latest version: https://lowemo.photo/lightroom-lua-utils 14 | 15 | This file is used in a few Lightroom plugins. 16 | 17 | This code is released under a Creative Commons CC-BY "Attribution" License: 18 | http://creativecommons.org/licenses/by/3.0/deed.en_US 19 | 20 | This bundle may be used for any purpose, provided that the copyright notice 21 | and web-page links, above, as well as the 'AUTHOR_NOTE' string, below are 22 | maintained. Enjoy. 23 | ------------------------------------------------------------------------------]] 24 | 25 | local LUTILS = {} 26 | 27 | LUTILS.VERSION = 20161121.02 -- version history at end of file 28 | LUTILS.AUTHOR_NOTE = "LUTILS.lua--Lua utility functions by Lowell Montgomery (https://lowemo.photo/lightroom-lua-utils) version: " .. LUTILS.VERSION 29 | 30 | -- The following provides an 80 character-width attribution text that can be inserted for display 31 | -- in a plugin derived using these helper functions. 32 | LUTILS.Attribution = "This plugin uses LUTILS, Lua utilities, © 2016 by Lowell Montgomery\n (https://lowemo.photo/lightroom-lua-utils) version: " .. LUTILS.VERSION .. "\n\nThis code is released under a Creative Commons CC-BY “Attribution” License:\n http://creativecommons.org/licenses/by/3.0/deed.en_US" 33 | 34 | -- Check simple table for a given value's presence 35 | function LUTILS.inTable (val, t) 36 | if type(t) ~= "table" then 37 | return false 38 | else 39 | for i, tval in pairs(t) do 40 | if val == tval then return true end 41 | end 42 | end 43 | return false; 44 | end 45 | 46 | -- Given a string and delimiter (e.g. ', '), break the string into parts and return as table 47 | -- This works like PHP's explode() function. 48 | function LUTILS.split(s, delim) 49 | if (delim == '') then return false end 50 | local pos = 0 51 | local t = {} 52 | -- For each delimiter found, add to return table 53 | for st, sp in function() return string.find(s, delim, pos, true) end do 54 | -- Get chars to next delimiter and insert in return table 55 | t[#t + 1] = string.sub(s, pos, st - 1) 56 | -- Move past the delimiter 57 | pos = sp + 1 58 | end 59 | -- Get chars after last delimiter and insert in return table 60 | t[#t + 1] = string.sub(s, pos) 61 | 62 | return t; 63 | end 64 | 65 | -- Merge two tables (like PHP array_merge()) 66 | function LUTILS.tableMerge(table1, table2) 67 | for i=1,#table2 do 68 | table1[#table1 + 1] = table2[i] 69 | end 70 | return table1; 71 | end 72 | 73 | -- Basic trim functionality to remove whitespace from either end of a string 74 | function LUTILS.trim(s) 75 | if s == nil then return nil end 76 | return string.gsub(s, '^%s*(.-)%s*$', '%1'); 77 | end 78 | 79 | return LUTILS; 80 | 81 | -- 20161101.01 Initial pre-release version 82 | -- 20161121.02 2nd Pre-release version; only minor changes. 83 | -------------------------------------------------------------------------------- /ClarifaiTagger.lrdevplugin/KwUtils.lua: -------------------------------------------------------------------------------- 1 | --[[---------------------------------------------------------------------------- 2 | 3 | KwUtils.lua 4 | Utility functions for Lightroom Keywords 5 | 6 | -------------------------------------------------------------------------------- 7 | 8 | Copyright 2016 Lowell "LoweMo / LoMo" Montgomery 9 | https://lowemo.photo 10 | Latest version: https://lowemo.photo/lightroom-keyword-utils 11 | 12 | This file is used in a few Lightroom plugins. 13 | 14 | This code is released under a Creative Commons CC-BY "Attribution" License: 15 | http://creativecommons.org/licenses/by/3.0/deed.en_US 16 | 17 | This bundle may be used for any purpose, provided that the copyright notice 18 | and web-page links, above, as well as the 'AUTHOR_NOTE' and 'Attribution' 19 | strings, below are maintained. Enjoy. 20 | ------------------------------------------------------------------------------]] 21 | 22 | local LUTILS = require 'LUTILS' 23 | 24 | KwUtils = {} 25 | KwUtils.catKws = nil 26 | KwUtils.catKwPaths = nil 27 | 28 | KwUtils.VERSION = 20161126.03 -- version history at end of file 29 | KwUtils.AUTHOR_NOTE = "KwUtils.lua is a set of Lightroom keyword utility functions, © 2016 by Lowell Montgomery (https://lowemo.photo/lightroom-keyword-utils) version: " .. KwUtils.VERSION 30 | 31 | -- The following provides an 80 character-width attribution text that can be inserted for display 32 | -- in a plugin derived using these helper functions. 33 | KwUtils.Attribution = "This plugin uses KwUtils, Lightroom keyword utilities, © 2016 by Lowell Montgomery\n (https://lowemo.photo/lightroom-keyword-utils) version: " .. KwUtils.VERSION .. "\n\nThis code is released under a Creative Commons CC-BY “Attribution” License:\n http://creativecommons.org/licenses/by/3.0/deed.en_US" 34 | 35 | function KwUtils.addKeywordWithParents(photo, keyword) 36 | photo:addKeyword(keyword) 37 | local parent = keyword:getParent() 38 | if parent ~= nil then 39 | KwUtils.addKeywordWithParents(photo, parent) 40 | end 41 | end 42 | 43 | -- Call this function with just a keyword object. This recursively calls kw:getParent, 44 | -- adding each parent to the table of parent keywords. When the top of the hierarchy 45 | -- is reached, the "ancestry table" of keywords is returned. 46 | function KwUtils.getAllParentKeywords(kw, parents) 47 | -- Set parents to empty table if not already existing 48 | parents = parents ~= nil and parents or {} 49 | local p = kw:getParent() 50 | if p ~= nil then 51 | parents[#parents+1] = p 52 | KwUtils.getAllParentKeywords(p, parents) 53 | end 54 | return parents 55 | end 56 | 57 | -- A photo may have keywords selected without the parent keywords actually being 58 | -- selected. Although any parents not set to be suppressed on export will be 59 | -- included during the export process, the photo will not show up in the library 60 | -- if you filter by the such a term. This function explicitly adds all keyword parents. 61 | function KwUtils.addAllKeywordParentsForPhoto(photo) 62 | local keywordsForPhoto = photo:getRawMetadata('keywords') 63 | 64 | local keywordsToAdd = {} 65 | for _,kw in ipairs(keywordsForPhoto) do 66 | local kwParents = KwUtils.getAllParentKeywords(kw) 67 | 68 | if (kwParents ~= nil) and (type(kwParents) == 'table') and (#kwParents ~= 0) then 69 | for _,parentKey in ipairs(kwParents) do 70 | if (not LUTILS.inTable(parentKey, keywordsForPhoto)) and (not LUTILS.inTable(parentKey, keywordsToAdd)) then 71 | keywordsToAdd[#keywordsToAdd+1] = parentKey 72 | end 73 | end 74 | end 75 | end 76 | for _,kwToAdd in ipairs(keywordsToAdd) do 77 | photo:addKeyword(kwToAdd) 78 | end 79 | -- Return the keywords which have been added 80 | return keywordsToAdd 81 | end 82 | 83 | -- Add or remove a keyword based on the "state" of the associated checkbox. 84 | -- Presumed is that we call this when the state differs from what is already on this image, 85 | -- i.e. that they keyword is being changed for the photo (added or removed) 86 | function KwUtils.addOrRemoveKeyword(photo, keyword, state) 87 | if state then 88 | KwUtils.addKeywordWithParents(photo, keyword) 89 | else 90 | -- We cannot assume parents should be removed if already there. 91 | photo:removeKeyword(keyword) 92 | end 93 | end 94 | 95 | --Returns array of keywords with a given name 96 | function KwUtils.getAllKeywordsByName(name, keywords, found) 97 | found = found or {} 98 | if type(found) == 'LrKeyword' then 99 | found = {found} 100 | elseif type(found) ~= 'table' then 101 | found = {} 102 | end 103 | for i, kw in pairs(keywords) do 104 | -- If we have found the keyword we want, return it: 105 | if kw:getName() == name and LUTILS.inTable(kw, found) == false then 106 | found[#found + 1] = kw 107 | -- Otherwise, use recursion to check next level if kw has child keywords: 108 | else 109 | local kchildren = kw:getChildren() 110 | if #kchildren > 0 then 111 | found = KwUtils.getAllKeywordsByName(name, kchildren, found) 112 | end 113 | end 114 | end 115 | -- By now, we should have them all 116 | return found 117 | end 118 | 119 | 120 | -- Gets string representing a keywords parent names in hierarchical order, e.g. 121 | -- "TOP_LEVEL_CATEGORY | second_level_parent | parent" 122 | function KwUtils.getAncestryString(kw, ancestryString) 123 | ancestryString = ancestryString or '' 124 | local parent = kw:getParent() 125 | if parent ~= nil then 126 | ancestryString = parent:getName() .. " | " .. ancestryString 127 | ancestryString = KwUtils.getAncestryString(parent, ancestryString) 128 | end 129 | return ancestryString; 130 | end 131 | 132 | -- Return a comma-separated string listing all children of a term 133 | function KwUtils.getChildrenString(kw) 134 | local childNamesTable = KwUtils.getKeywordChildNamesTable(kw) 135 | if #childNamesTable > 0 then 136 | return table.concat(childNamesTable, ", ") 137 | else return "" 138 | end 139 | end 140 | 141 | -- Find a keyword by a given name within a table of keyword objects 142 | -- arg: lookfor Name of keyword to search for 143 | -- arg: keywordSet Table of keywords, usually the top level keywords returned by Lightroom 144 | -- API call to catalog:getKeywords(). If the sought keyword is not found 145 | -- any "branches" of child terms are also examined (by recursive calls to this function) 146 | function KwUtils.getKeywordByName(lookfor, keywordSet) 147 | for i, kw in pairs(keywordSet) do 148 | -- If we have found the keyword we want, return it: 149 | if kw:getName() == lookfor then 150 | return kw 151 | -- Otherwise, use recursion to check next level if kw has child keywords: 152 | else 153 | local kchildren = kw:getChildren() 154 | if kchildren and #kchildren > 0 then 155 | local nextkw = KwUtils.getKeywordByName(lookfor, kchildren) 156 | if nextkw ~= nil then 157 | return nextkw 158 | end 159 | end 160 | end 161 | end 162 | -- If we have not returned the sought keyword, it's not there: 163 | return nil 164 | end 165 | 166 | --General Lightroom API helper functions for keywords 167 | function KwUtils.getKeywordChildNamesTable(parentKey) 168 | local kchildren = parentKey:getChildren() 169 | local childNames = {} 170 | if kchildren and #kchildren > 0 then 171 | childNames = KwUtils.getKeywordNames(kchildren) 172 | end 173 | -- Return the table of child terms (empty if no child terms for passed keyword) 174 | return childNames 175 | end 176 | 177 | -- Get names of all Keyword objects in a table 178 | function KwUtils.getKeywordNames(keywords) 179 | local names = {} 180 | if type(keywords) == 'table' then 181 | for _,kw in ipairs(keywords) do 182 | names[#names+1] = kw:getName() 183 | end 184 | end 185 | return names 186 | end 187 | 188 | 189 | -- This is used by the KwUtils.findAllKeywords and allows skipping over branches that 190 | -- would not be helpful for whatever purpose. A plugin does not NEED to implement 191 | -- the ignore_keyword_branches preference. If it does not exist, we skip no branches, 192 | -- just as if it did exist, but the field was empty. 193 | function KwUtils.getIgnoreKeywordsTable() 194 | local prefs = import 'LrPrefs'.prefsForPlugin(_PLUGIN.id) 195 | local ignoreKeywordsList = '' 196 | if prefs.ignore_keyword_branches ~= nil then 197 | ignoreKeywordsList = prefs.ignore_keyword_branches 198 | end 199 | local ignoreKeysTable = LUTILS.split(ignoreKeywordsList, ', ') 200 | for i, kw in ipairs(ignoreKeysTable) do 201 | local val = LUTILS.trim(kw) 202 | if val == '' then 203 | ignoreKeysTable[i] = nil 204 | else 205 | ignoreKeysTable[i] = val 206 | end 207 | end 208 | return ignoreKeysTable 209 | end 210 | 211 | -- This function must be called from within an asynchronous task started using LrTasks. 212 | function KwUtils.getAllKeywords(catalog) 213 | if KwUtils.catKws == nil then 214 | KwUtils.catKws = {} 215 | KwUtils.catKwPaths = {} 216 | local topLevelKeywords = catalog:getKeywords() 217 | return KwUtils.findAllKeywords(topLevelKeywords) 218 | end 219 | return KwUtils.catKws 220 | end 221 | 222 | -- Given a set of keywords (normally starting with a top level of a hierarchy), 223 | -- get all keywords in the set with any child/descendant keywords) and populate 224 | -- our top-level keyword table variables with data we can quickly use. 225 | function KwUtils.findAllKeywords(keywords, kpath) 226 | kpath = kpath or '' 227 | local ignoreKeysTable = KwUtils.getIgnoreKeywordsTable() 228 | for _, kw in pairs(keywords) do 229 | local name = kw:getName() 230 | -- Skip any keywords (and descendants) listed in the ignoreKeysTable 231 | if not LUTILS.inTable(name, ignoreKeysTable) then 232 | local keyname = string.lower(name) 233 | if KwUtils.catKws[keyname] ~= nil then 234 | local count = #KwUtils.catKws[keyname] 235 | KwUtils.catKws[keyname][count + 1] = kw 236 | KwUtils.catKwPaths[keyname][count + 1] = kpath 237 | else 238 | KwUtils.catKws[keyname] = {kw} 239 | KwUtils.catKwPaths[keyname] = {kpath} 240 | end 241 | local kids = kw:getChildren() 242 | if kids and #kids > 0 then 243 | local new_kpath = kpath ~= '' and kpath .. ' | ' .. name or name 244 | KwUtils.findAllKeywords(kids, new_kpath) 245 | end 246 | end 247 | end 248 | return KwUtils.catKws; 249 | end 250 | 251 | -- Get number of keywords by a given name (adjusted to lower case) or false 252 | -- if the keyword does not exist. This functionality depends on first running 253 | -- KwUtils.findAllKeywords() to populate the catKws table. 254 | function KwUtils.keywordExists(keyword) 255 | if KwUtils.catKws[string.lower(keyword)] ~= nil then 256 | return #KwUtils.catKws[string.lower(keyword)] 257 | end 258 | return false 259 | end 260 | 261 | 262 | -- Get existing keywords for a photo which are not in a given set (table) 263 | function KwUtils.getOtherKeywords(photo, keywordNames) 264 | local photoKeywordList = photo:getFormattedMetadata('keywordTags') 265 | local photoKeywordNames = LUTILS.split(photoKeywordList, ', ') 266 | local ret = {} 267 | 268 | for _, keyName in ipairs(photoKeywordNames) do 269 | if not LUTILS.inTable(keyName, keywordNames) then 270 | ret[#ret + 1] = keyName 271 | end 272 | end 273 | return ret 274 | end 275 | 276 | -- Check for actual keyword (by keyword object, not name) associated with a photo 277 | -- Given a catalog photo, check for the presence of any given keyword. 278 | function KwUtils.hasKeywordById(photo, keyword) 279 | local keywordsForPhoto = photo:getRawMetadata('keywords') 280 | --Look for keyword object passed in array of Keyword objects. 281 | return LUTILS.inTable(keyword, keywordsForPhoto) 282 | end 283 | 284 | -- Check if photo already has a particular keyword (by name) 285 | -- Converts all keywords to lower case before comparison which "ignores case" 286 | function KwUtils.hasKeywordByName(photo, keywordName) 287 | local photoKeywordList = string.lower(photo:getFormattedMetadata('keywordTags')) 288 | local keywordNamesTable = LUTILS.split(photoKeywordList, ', ') 289 | return LUTILS.inTable(string.lower(keywordName), keywordNamesTable) 290 | end 291 | 292 | return KwUtils 293 | 294 | -- 20161101.01 Initial release. 295 | -- It includes functions I had found myself writing and re-writing in various plugins. 296 | -- 20161122.02 Minor tweaks and bugfixes; not yet officially released, so will not itemize changes 297 | -- 20161126.03 Moved more logic into the KwUtils. It is no longer a local bundle, so this may have ramifications for memory 298 | -- etc, but allows us to call "expenive" processes (new findAllKeywords() / getAllKeywords() functionality) and 299 | -- hopefully be able to access the results from other scripts in a plugin. Still "pre-release" 300 | -------------------------------------------------------------------------------- /ClarifaiTagger.lrdevplugin/TaggerDialog.lua: -------------------------------------------------------------------------------- 1 | local LrApplication = import 'LrApplication' 2 | local LrBinding = import "LrBinding" 3 | local LrColor = import 'LrColor' 4 | local LrDialogs = import 'LrDialogs' 5 | local LrFileUtils = import 'LrFileUtils' 6 | local LrFunctionContext = import "LrFunctionContext" 7 | local LrLogger = import 'LrLogger' 8 | local LrPathUtils = import 'LrPathUtils' 9 | local prefs = import 'LrPrefs'.prefsForPlugin(_PLUGIN.id) 10 | local LrTasks = import 'LrTasks' 11 | local LrView = import "LrView" 12 | local ClarifaiAPI = require 'ClarifaiAPI' 13 | local KwUtils = require 'KwUtils' 14 | local LUTILS = require 'LUTILS' 15 | 16 | local logger = LrLogger('ClarifaiAPI') 17 | logger:enable('print') 18 | 19 | ----------------------------------------- 20 | -- Returns a checkbox label used in the dialog. i, j, and k are normally all integers 21 | local function getCheckboxLabel(i, j, k) 22 | return 'check_' .. tostring(i) .. '_' .. tostring(j) .. '_' .. tostring(k) 23 | end 24 | 25 | local function makeCheckbox(i, j, k, tagName, prob, boldKeywords, showProbability) 26 | local f = LrView.osFactory() 27 | -- Tooltip should show the hierarchical level of a keyword 28 | local tt = '' 29 | local lowerkey = string.lower(tagName) 30 | if KwUtils.catKwPaths[lowerkey] ~= nil and KwUtils.catKwPaths[lowerkey][k] == '' then 31 | tt = '(In the keyword root level)' 32 | elseif KwUtils.catKwPaths[lowerkey] ~= nil then 33 | tt = '(In ' .. KwUtils.catKwPaths[lowerkey][k] .. ')' 34 | else -- KwUtils.catKwPaths[lowerkey] == nil 35 | tt = "New keyword by the name “”" .. tagName .. "” will be created by selecting this tag." 36 | end 37 | 38 | local checkbox = { 39 | title = tagName, 40 | tooltip = tt, 41 | value = LrView.bind(getCheckboxLabel(i, j, k)), 42 | } 43 | 44 | if boldKeywords then 45 | checkbox.font = ''; 46 | end 47 | 48 | if not showProbability then 49 | return f:checkbox(checkbox) 50 | end 51 | 52 | return f:row { 53 | f:checkbox(checkbox), 54 | f:static_text { 55 | title = string.format('(%2.1f)', prob * 100), 56 | text_color = LrColor(0.5, 0.5, 0.5), 57 | } 58 | } 59 | end 60 | 61 | -- Builds the tagger Dialog window 62 | local function makeWindow(catalog, photos, json) 63 | local results = json['results'] 64 | -- for _, result in ipairs(results) do 65 | -- local cs = result['result']['tag']['classes'] 66 | -- end 67 | 68 | local boldExistingKeywords = prefs.boldExistingKeywords 69 | local autoSelectExistingKeywords = prefs.autoSelectExistingKeywords 70 | local showProbability = prefs.showProbability 71 | 72 | LrFunctionContext.callWithContext('showDialog', function(context) 73 | local f = LrView.osFactory() 74 | local bind = LrView.bind 75 | 76 | local properties = LrBinding.makePropertyTable(context) 77 | 78 | local columns = {} 79 | for i, photo in ipairs(photos) do 80 | -- local keywords = json['results'][i]['result']['tag']['classes'] 81 | -- local probs = json['results'][i]['result']['tag']['probs'] 82 | logger:info(' concepts ', i); 83 | local keywords = {} 84 | local probs = {} 85 | local concepts = json['outputs'][i]['data']['concepts'] 86 | for i, concept in ipairs(concepts) do 87 | -- logger:info(' concepts: ', concept['name']) 88 | table.insert(keywords, concept['name']) 89 | table.insert(probs, concept['value']) 90 | end 91 | 92 | 93 | local tbl = { 94 | spacing = f:label_spacing(8), 95 | bind_to_object = properties, 96 | f:catalog_photo { 97 | width = prefs.thumbnailSize, 98 | photo = photo, 99 | } 100 | } 101 | local previewWidth = prefs.imagePreviewWindowWidth; 102 | local previewHeight = prefs.imagePreviewWindowHeight; 103 | local previewButtonTt = "Open larger preview (in " .. previewWidth .. " x " .. previewHeight .. "px window)"; 104 | tbl[#tbl + 1] = f:row { 105 | f:push_button { 106 | title = 'View Full Size Image', 107 | tooltip = previewButtonTt, 108 | action = function (clickedview) 109 | LrDialogs.presentModalDialog({ 110 | title = 'Review Image', 111 | contents = f:catalog_photo { 112 | photo = photo, 113 | width = previewWidth, 114 | height = previewHeight, 115 | tooltip = "Press “Enter” key to close if the “Close Window” button is off-screen", 116 | }, 117 | cancelVerb = '< exclude >', 118 | actionVerb = 'Close Window', 119 | }); 120 | end 121 | } 122 | } 123 | 124 | for j = 1, #keywords do 125 | local lowerkey = string.lower(keywords[j]) 126 | local numKeysByName = KwUtils.catKws[lowerkey] ~= nil and #KwUtils.catKws[lowerkey] or false 127 | 128 | -- Make sure we are selecting checkboxes for keywords already on a photo: 129 | local selectKeyword = KwUtils.hasKeywordByName(photo, keywords[j]) 130 | 131 | local boldKeyword = false; 132 | local kwExists = (KwUtils.keywordExists(keywords[j]) ~= false) and true or false 133 | 134 | if boldExistingKeywords or autoSelectExistingKeywords then 135 | -- Does the keyword list include the keyword 136 | if boldExistingKeywords then 137 | boldKeyword = kwExists 138 | end 139 | -- Probability from Clarifai actually expressed as fraction of one. 140 | local prob = tonumber(probs[j]) * 100 141 | if autoSelectExistingKeywords and prob >= tonumber(prefs.autoSelectProbabilityThreshold) then 142 | selectKeyword = kwExists 143 | end 144 | end 145 | if numKeysByName ~= false then 146 | for k=1, numKeysByName do 147 | local keyword = KwUtils.catKws[lowerkey][k] 148 | properties[getCheckboxLabel(i, j, k)] = selectKeyword 149 | tbl[#tbl + 1] = makeCheckbox(i, j, k, keywords[j], probs[j], boldKeyword, showProbability) 150 | end 151 | else 152 | local k = 0 153 | -- It is a new keyword so will not be selected automatically 154 | properties[getCheckboxLabel(i, j, k)] = false 155 | tbl[#tbl + 1] = makeCheckbox(i, j, k, keywords[j], probs[j], boldKeyword, showProbability) 156 | end 157 | end 158 | 159 | local otherKeywords = KwUtils.getOtherKeywords(photo, keywords) 160 | if #otherKeywords > 0 then 161 | tbl[#tbl + 1] = f:spacer { 162 | height = 4 163 | } 164 | for _, o in ipairs(otherKeywords) do 165 | tbl[#tbl + 1] = f:static_text { 166 | title = ' ' .. o, 167 | text_color = LrColor(0.3, 0.3, 0.3), 168 | } 169 | end 170 | end 171 | 172 | columns[i] = f:column(tbl); 173 | end 174 | 175 | local contents = f:scrolled_view { 176 | width = prefs.taggingWindowWidth, 177 | height = prefs.taggingWindowHeight, 178 | background_color = LrColor(0.9, 0.9, 0.9), 179 | f:row(columns) 180 | } 181 | 182 | local result = LrDialogs.presentModalDialog({ 183 | title = LOC '$$$/ClarifaiTagger/TaggerWindow/Title=Clarifai Tagger', 184 | contents = contents, 185 | actionVerb = 'Save', 186 | }) 187 | 188 | if result == 'ok' then 189 | local newKeywords = {} 190 | catalog:withWriteAccessDo('writePhotosKeywords', function(context) 191 | for i, photo in ipairs(photos) do 192 | -- local keywords = json['results'][i]['result']['tag']['classes'] 193 | 194 | local keywords = {} 195 | -- local probs = {} 196 | local concepts = json['outputs'][i]['data']['concepts'] 197 | for i, concept in ipairs(concepts) do 198 | logger:info(' concepts: ', concept['name']) 199 | table.insert(keywords, concept['name']) 200 | -- table.insert(probs, concept['value']) 201 | end 202 | 203 | for j = 1, #keywords do 204 | local kwName = keywords[j] 205 | local kwLower = string.lower(kwName) 206 | local keywordsByName = KwUtils.catKws[kwLower] 207 | local numKeysByName = keywordsByName ~= nil and #keywordsByName or 0 208 | 209 | -- First deal with the issue of adding a keyword that was not in the Lightroom library before: 210 | if numKeysByName == 0 then 211 | local checkboxState = properties[getCheckboxLabel(i, j, 0)] 212 | if checkboxState ~= false then 213 | -- catalog:createKeyword( keywordName, synonyms, includeOnExport, parent, returnExisting ) 214 | local keyword = catalog:createKeyword(kwName, {}, true, nil, true) 215 | if keyword == false then 216 | -- Keyword created in current withWriteAccessDo block, so is inaccessible via `returnExisting`. 217 | keyword = newKeywords[kwName] 218 | else 219 | newKeywords[kwName] = keyword 220 | end 221 | photo:addKeyword(keyword) 222 | 223 | end 224 | 225 | -- Not a new term, but only one checkbox exists for the term: 226 | else 227 | for k=1, numKeysByName do 228 | local checkboxState = properties[getCheckboxLabel(i, j, k)] 229 | local keyword = KwUtils.catKws[kwLower][k] 230 | if numKeysByName == 1 and checkboxState ~= KwUtils.hasKeywordByName(photo, kwName) then 231 | KwUtils.addOrRemoveKeyword(photo, keyword, checkboxState) 232 | 233 | elseif numKeysByName > 1 then 234 | -- We need to use more accurate (less performant) means to verify the actual keyword 235 | -- is (or is not) already associated with the photo. 236 | if checkboxState ~= KwUtils.hasKeywordById(photo, keyword) then 237 | KwUtils.addOrRemoveKeyword(photo, keyword, checkboxState) 238 | end 239 | end 240 | end 241 | end 242 | end 243 | end 244 | end ) 245 | end 246 | end ) 247 | end 248 | 249 | local function requestJpegThumbnails(target_photos, processed_photos, generated_thumbnails, callback) 250 | local count = #target_photos 251 | if count == 0 then 252 | callback(processed_photos, generated_thumbnails) 253 | return 254 | end 255 | 256 | local photo = target_photos[count] 257 | table.remove(target_photos, count) 258 | 259 | local f = function(jpg, err) 260 | if err == nil then 261 | processed_photos[#processed_photos + 1] = photo 262 | generated_thumbnails[#generated_thumbnails + 1] = jpg 263 | requestJpegThumbnails(target_photos, processed_photos, generated_thumbnails, callback) 264 | end 265 | end 266 | 267 | local imageSize = tonumber(prefs.imageSize) or 400 268 | photo:requestJpegThumbnail(imageSize, imageSize, f) 269 | end 270 | 271 | function reverseArray(array) 272 | local reversed = {} 273 | for idx, val in ipairs(array) do 274 | reversed[#array - idx + 1] = val 275 | end 276 | return reversed 277 | end 278 | 279 | local thumbnailDir = LrPathUtils.getStandardFilePath('temp') 280 | 281 | LrTasks.startAsyncTask(function() 282 | local catalog = LrApplication.activeCatalog() 283 | local photos = reverseArray(catalog:getTargetPhotos()) 284 | 285 | local limitSize = 128 -- currently Clarifai's max_batch_size 286 | if #photos > limitSize then 287 | local message = LOC '$$$/ClarifaiTagger/TaggerWindow/ExceedsBatchSizeMessage=Selected photos execeeds the limit (%d).' 288 | local info = LOC '$$$/ClarifaiTagger/TaggerWindow/ExceedsBatchSizeInfo=%d photos are selected currently.' 289 | LrDialogs.message(string.format(message, limitSize), string.format(info, #photos), 'warning') 290 | return 291 | end 292 | 293 | requestJpegThumbnails(photos, {}, {}, function(photos, thumbnails) 294 | logger:info(' thumbnail created ', #photos, #thumbnails) 295 | local thumbnailPaths = {} 296 | for idx, thumbnail in ipairs(thumbnails) do 297 | local photo = photos[idx] 298 | local filePath = photo.path -- photo:getRawMetadata('path'); 299 | local fileName = LrPathUtils.leafName(filePath) 300 | local path = LrPathUtils.child(thumbnailDir, fileName) 301 | local jpg_path = LrPathUtils.addExtension(path, 'jpg') 302 | logger:info(' jpg_path ', jpg_path) 303 | local out = io.open(jpg_path, 'wb') 304 | io.output(out) 305 | io.write(thumbnail) 306 | io.close(out) 307 | thumbnailPaths[#thumbnailPaths + 1] = jpg_path 308 | end 309 | 310 | LrTasks.startAsyncTask(function() 311 | local message = LOC '$$$/ClarifaiTagger/TaggerWindow/ProcessingMessage=Sending thumbnails of the selected photos...' 312 | LrDialogs.showBezel(message, 2) 313 | 314 | local json = ClarifaiAPI.getTags(photos, thumbnailPaths) 315 | 316 | -- Populate the KwUtils.catKws and KwUtils.catKwPaths tables 317 | local allKeys = KwUtils.getAllKeywords(catalog) 318 | makeWindow(catalog, photos, json) 319 | 320 | for _, thumbnailPath in ipairs(thumbnailPaths) do 321 | LrFileUtils.delete(thumbnailPath) 322 | end 323 | end ) 324 | end ) 325 | end) 326 | -------------------------------------------------------------------------------- /ClarifaiTagger.lrdevplugin/ClarifaiTaggerInfoProvider.lua: -------------------------------------------------------------------------------- 1 | local LrView = import 'LrView' 2 | 3 | local simpleJsonAcknowledgement = 'Simple JSON encoding and decoding in pure Lua.\n\nCopyright 2010-2014 Jeffrey Friedl\nhttp://regex.info/blog/\n\nLatest version: http://regex.info/blog/lua/json\n\nThis code is released under a Creative Commons CC-BY "Attribution" License:\nhttp://creativecommons.org/licenses/by/3.0/deed.en_US' 4 | 5 | ----------------------------------------- 6 | local ClarifaiTaggerInfoProvider = {} 7 | 8 | function ClarifaiTaggerInfoProvider.sectionsForTopOfDialog(viewFactory, propertyTable) 9 | local prefs = import 'LrPrefs'.prefsForPlugin(_PLUGIN.id) 10 | local bind = LrView.bind 11 | local share = LrView.share 12 | 13 | return { 14 | { 15 | title = LOC '$$$/ClarifaiTagger/Settings/AuthHeader=Authentication Settings', 16 | 17 | viewFactory:row { 18 | spacing = viewFactory:label_spacing(), 19 | 20 | viewFactory:static_text { 21 | tooltip = "Copy from your Clarifai Account https://developer.clarifai.com/account/api-keys.", 22 | title = LOC '$$$/ClarifaiTagger/Settings/Heading=You need to create an account on clarifai.ai and create a new API key.', 23 | alignment = 'right', 24 | -- width = share 'title_width', 25 | }, 26 | }, 27 | 28 | viewFactory:row { 29 | spacing = viewFactory:label_spacing(), 30 | 31 | viewFactory:static_text { 32 | tooltip = "Copy from your Clarifai Account https://developer.clarifai.com/account/api-keys.", 33 | title = LOC '$$$/ClarifaiTagger/Settings/ClientId=API KEY:', 34 | alignment = 'right', 35 | -- width = share 'title_width', 36 | }, 37 | 38 | viewFactory:edit_field { 39 | tooltip = "Copy from the setup page on Clarifai.com for your Clarifai application.", 40 | fill_horizonal = 1, 41 | width_in_chars = 35, 42 | alignment = 'left', 43 | value = bind { key = 'clientId', object = prefs }, 44 | }, 45 | }, 46 | 47 | -- viewFactory:row { 48 | -- spacing = viewFactory:label_spacing(), 49 | -- 50 | -- viewFactory:static_text { 51 | -- tooltip = "Copy from the setup page on Clarifai.com for your Clarifai application.", 52 | -- title = LOC '$$$/ClarifaiTagger/Settings/clientSecret=Client Secret:', 53 | -- alignment = 'right', 54 | -- -- width = share 'title_width', 55 | -- }, 56 | -- 57 | -- viewFactory:edit_field { 58 | -- tooltip = "Copy from the setup page on Clarifai.com for your Clarifai application.", 59 | -- fill_horizonal = 1, 60 | -- width_in_chars = 35, 61 | -- alignment = 'left', 62 | -- value = bind { key = 'clientSecret', object = prefs }, 63 | -- }, 64 | -- }, 65 | 66 | -- viewFactory:row { 67 | -- spacing = viewFactory:label_spacing(), 68 | -- 69 | -- viewFactory:static_text { 70 | -- title = LOC '$$$/ClarifaiTagger/Settings/AccessToken=Access Token:', 71 | -- alignment = 'right', 72 | -- -- width = share 'title_width', 73 | -- }, 74 | -- 75 | -- viewFactory:edit_field { 76 | -- fill_horizonal = 1, 77 | -- width_in_chars = 35, 78 | -- alignment = 'left', 79 | -- enabled = false, 80 | -- value = bind { key = 'accessToken', object = prefs }, 81 | -- }, 82 | -- }, 83 | -- viewFactory:separator { fill_horizontal = 1 }, 84 | }, 85 | 86 | { 87 | title = LOC '$$$/ClarifaiTagger/Settings/tagging=Tagging Dialog', 88 | 89 | viewFactory:row { 90 | spacing = viewFactory:control_spacing(), 91 | 92 | viewFactory:static_text { 93 | title = LOC '$$$/ClarifaiTagger/Settings/thumbnailSize=Thumbnail size (in tagging dialog)', 94 | alignment = 'left', 95 | width = share 'title_width', 96 | }, 97 | 98 | viewFactory:slider { 99 | min = 250, 100 | max = 500, 101 | integral = true, 102 | alignment = 'left', 103 | tooltip = 'Allowable range 250 to 500 pixels.', 104 | value = bind { key = 'thumbnailSize', object = prefs }, 105 | }, 106 | 107 | viewFactory:edit_field { 108 | fill_horizonal = 1, 109 | width_in_chars = 3, 110 | min = 250, 111 | max = 500, 112 | increment = 1, 113 | precision = 0, 114 | alignment = 'left', 115 | tooltip = 'Allowable range 250 to 500 pixels. “Short side” dimension for thumbnail images shown in the tagging window', 116 | value = bind { key = 'thumbnailSize', object = prefs }, 117 | }, 118 | }, 119 | viewFactory:row { 120 | viewFactory:spacer { width = share 'title_width', height = 1 }, 121 | 122 | viewFactory:static_text { 123 | title = LOC '$$$/ClarifaiTagger/Settings/ThumbnailSizeDesc=Size of tagging window thumbnail images (“short side”)', 124 | alignment = 'right', 125 | }, 126 | }, 127 | viewFactory:spacer { width = 1, height = 4 }, 128 | viewFactory:row { 129 | spacing = viewFactory:control_spacing(), 130 | viewFactory:static_text { 131 | title = 'Tagging window width', 132 | tooltip = 'Width (px) of the tagging window (range 500–3800px)', 133 | }, 134 | viewFactory:edit_field { 135 | value = bind { key = 'taggingWindowWidth', object = prefs }, 136 | tooltip = 'Width (px) of the tagging window (range 500–3800px)', 137 | min = 500, 138 | max = 3800, 139 | width_in_chars = 4, 140 | increment = 1, 141 | precision = 0, 142 | }, 143 | spacing = viewFactory:control_spacing(), 144 | viewFactory:static_text { 145 | title = 'Tagging window height', 146 | tooltip = 'Height (px) of the tagging window (range 400-2100px)', 147 | }, 148 | viewFactory:edit_field { 149 | value = bind { key = 'taggingWindowHeight', object = prefs }, 150 | tooltip = 'Height (px) of the tagging window (range 400–2100px)', 151 | min = 400, 152 | max = 2100, 153 | width_in_chars = 4, 154 | increment = 1, 155 | precision = 0, 156 | } 157 | }, 158 | viewFactory:spacer { width = 1, height = 4 }, 159 | viewFactory:row { 160 | spacing = viewFactory:control_spacing(), 161 | viewFactory:static_text { 162 | title = 'Image preview window width', 163 | tooltip = 'Width (px) of the image preview window (range 500–3800px)', 164 | }, 165 | viewFactory:edit_field { 166 | value = bind { key = 'imagePreviewWindowWidth', object = prefs }, 167 | tooltip = 'Width (px) of the image preview window (range 500–3800px)', 168 | min = 500, 169 | max = 3800, 170 | width_in_chars = 4, 171 | increment = 1, 172 | precision = 0, 173 | }, 174 | spacing = viewFactory:control_spacing(), 175 | viewFactory:static_text { 176 | title = 'Image preview window height', 177 | tooltip = 'Height (px) of the image preview window (range 400–2100px)', 178 | }, 179 | viewFactory:edit_field { 180 | value = bind { key = 'imagePreviewWindowHeight', object = prefs }, 181 | tooltip = 'Height (px) of the image preview window (range 400–2100px)', 182 | min = 400, 183 | max = 2100, 184 | width_in_chars = 4, 185 | increment = 1, 186 | precision = 0, 187 | } 188 | }, 189 | viewFactory:spacer { width = 1, height = 4 }, 190 | viewFactory:row { 191 | spacing = viewFactory:control_spacing(), 192 | 193 | viewFactory:checkbox { 194 | title = LOC '$$$/ClarifaiTagger/Settings/boldExistingKeywords=Show existing keywords in bold', 195 | tooltip = "Selecting this option will display in bold print any keywords which are already found in your keyword list", 196 | value = bind { key = 'boldExistingKeywords', object = prefs }, 197 | }, 198 | }, 199 | viewFactory:row { 200 | spacing = viewFactory:control_spacing(), 201 | 202 | viewFactory:checkbox { 203 | title = LOC '$$$/ClarifaiTagger/Settings/autoSelectExistingKeywords=Automatically Select Existing Keywords', 204 | tooltip = "Selecting this option will auto-select keyword checkboxes which would *not* create a new term in your keyword list.", 205 | value = bind { key = 'autoSelectExistingKeywords', object = prefs }, 206 | }, 207 | }, 208 | -- Probability threshold (only used if autoSelectExistingKeywords is turned on.) 209 | viewFactory:row { 210 | -- spacing = viewFactory:control_spacing(), 211 | spacing = viewFactory:label_spacing(), 212 | 213 | viewFactory:static_text { 214 | title = LOC '$$$/ClarifaiTagger/Settings/autoSelectProbabilityThreshold=Probability threshold for auto-selection:', 215 | alignment = 'left', 216 | width = share 'title_width', 217 | }, 218 | 219 | viewFactory:slider { 220 | min = 1, 221 | max = 99, 222 | integral = true, 223 | alignment = 'left', 224 | value = bind { key = 'autoSelectProbabilityThreshold', object = prefs }, 225 | }, 226 | 227 | viewFactory:edit_field { 228 | fill_horizonal = 1, 229 | width_in_chars = 2, 230 | min = 1, 231 | max = 99, 232 | increment = 1, 233 | precision = 0, 234 | alignment = 'left', 235 | tooltip = 'Setting for what level of Clarifai-rated probability is required for a keyword to be auto-selected.\n\nIgnored unless the "Auto-Select existing keywords" setting is selected.', 236 | value = bind { key = 'autoSelectProbabilityThreshold', object = prefs }, 237 | }, 238 | }, 239 | 240 | viewFactory:row { 241 | spacing = viewFactory:control_spacing(), 242 | 243 | viewFactory:checkbox { 244 | title = LOC '$$$/ClarifaiTagger/Settings/showProbability=Show Probability', 245 | tooltip = "Selecting this will display Clarifai's level of certainty that a keyword is accurate.", 246 | value = bind { key = 'showProbability', object = prefs }, 247 | }, 248 | }, 249 | 250 | viewFactory:row { 251 | spacing = viewFactory:label_spacing(), 252 | 253 | viewFactory:static_text { 254 | title = LOC '$$$/ClarifaiTagger/Settings/ignore_keyword_branches=Ignore keywords branches:', 255 | tooltip = 'Comma-separated list of keyword terms to ignore (including chilren and descendants).', 256 | alignment = 'left', 257 | width = share 'title_width', 258 | }, 259 | 260 | viewFactory:edit_field { 261 | tooltip = 'Comma-separated list of keyword terms to ignore (including chilren and descendants).', 262 | width_in_chars = 35, 263 | height_in_lines = 4, 264 | enabled = true, 265 | alignment = 'left', 266 | value = bind { key = 'ignore_keyword_branches', object = prefs }, 267 | }, 268 | }, 269 | }, 270 | 271 | { 272 | title = LOC '$$$/ClarifaiTagger/Settings/imageHeader=Image Settings', 273 | 274 | viewFactory:row { 275 | spacing = viewFactory:control_spacing(), 276 | 277 | viewFactory:static_text { 278 | title = LOC '$$$/ClarifaiTagger/Settings/imageSize=Image size (sent to Clarifai)', 279 | alignment = 'left', 280 | width = share 'title_width', 281 | }, 282 | 283 | viewFactory:slider { 284 | min = 400, 285 | max = 2000, 286 | integral = true, 287 | alignment = 'left', 288 | tooltip = 'Allowable range 400 to 2000 pixels. Higher values use more bandwidth, but may deliver more accurate results.', 289 | value = bind { key = 'imageSize', object = prefs }, 290 | }, 291 | 292 | viewFactory:edit_field { 293 | fill_horizonal = 1, 294 | width_in_chars = 4, 295 | min = 400, 296 | max = 2000, 297 | increment = 1, 298 | precision = 0, 299 | alignment = 'left', 300 | tooltip = 'Allowable range 400 to 2000 pixels. Higher values use more bandwidth, but may deliver more accurate results.', 301 | value = bind { key = 'imageSize', object = prefs }, 302 | }, 303 | }, 304 | 305 | viewFactory:row { 306 | viewFactory:spacer { width = share 'title_width', height = 1 }, 307 | 308 | viewFactory:static_text { 309 | title = LOC '$$$/ClarifaiTagger/Settings/ImageSizeDesc=Size of image sent to the Clarifai server', 310 | alignment = 'right', 311 | }, 312 | }, 313 | viewFactory:spacer { width = 1, height = 4 }, 314 | 315 | viewFactory:row { 316 | spacing = viewFactory:label_spacing(), 317 | 318 | viewFactory:static_text { 319 | title = LOC '$$$/ClarifaiTagger/Settings/keywordLanguage=Keyword language:', 320 | alignment = 'left', 321 | width = share 'title_width', 322 | }, 323 | 324 | viewFactory:popup_menu { 325 | items = { 326 | { value = '' , title = 'Default (depends on the server setting)' }, 327 | { value = 'ar' , title = 'Arabic (ar)' }, 328 | { value = 'bn' , title = 'Bengali (bn)' }, 329 | { value = 'da' , title = 'Danish (da)' }, 330 | { value = 'de' , title = 'German (de)' }, 331 | { value = 'en' , title = 'English (en)' }, 332 | { value = 'es' , title = 'Spanish (es)' }, 333 | { value = 'fi' , title = 'Finnish (fi)' }, 334 | { value = 'fr' , title = 'French (fr)' }, 335 | { value = 'hi' , title = 'Hindi (hi)' }, 336 | { value = 'hu' , title = 'Hungarian (hu)' }, 337 | { value = 'it' , title = 'Italian (it)' }, 338 | { value = 'ja' , title = 'Japanese (ja)' }, 339 | { value = 'ko' , title = 'Korean (ko)' }, 340 | { value = 'nl' , title = 'Dutch (nl)' }, 341 | { value = 'no' , title = 'Norwegian (no)' }, 342 | { value = 'pa' , title = 'Punjabi (pa)' }, 343 | { value = 'pl' , title = 'Polish (pl)' }, 344 | { value = 'pt' , title = 'Portuguese (pt)' }, 345 | { value = 'ru' , title = 'Russian (ru)' }, 346 | { value = 'sv' , title = 'Swedish (sv)' }, 347 | { value = 'tr' , title = 'Turkish (tr)' }, 348 | { value = 'zh-TW' , title = 'Chinese Traditional (zh-TW)' }, 349 | { value = 'zh' , title = 'Chinese Simplified (zh)' }, 350 | }, 351 | value = bind { key = 'keywordLanguage', object = prefs }, 352 | }, 353 | }, 354 | } 355 | } 356 | end 357 | 358 | 359 | function ClarifaiTaggerInfoProvider.sectionsForBottomOfDialog(viewFactory, propertyTable) 360 | local KwUtilsAttribution = require 'KwUtils'.Attribution 361 | local LutilsAttribution = require 'LUTILS'.Attribution 362 | return { 363 | { 364 | title = LOC '$$$/ClarifaiTagger/Settings/acknowledgements=Acknowledgements', 365 | viewFactory:static_text { 366 | title = LOC '$$$/ClarifaiTagger/Settings/simpleJSON=Simple JSON', 367 | font = '', 368 | }, 369 | viewFactory:edit_field { 370 | width_in_chars = 80, 371 | height_in_lines = 9, 372 | enabled = false, 373 | value = simpleJsonAcknowledgement 374 | }, 375 | viewFactory:static_text { 376 | title = LOC '$$$/ClarifaiTagger/Settings/KwUtils=KwUtils: Keyword Utility Functions for Lightroom', 377 | font = '', 378 | }, 379 | viewFactory:edit_field { 380 | width_in_chars = 80, 381 | height_in_lines = 5, 382 | enabled = false, 383 | value = KwUtilsAttribution 384 | }, 385 | viewFactory:static_text { 386 | title = LOC '$$$/ClarifaiTagger/Settings/Lutils=LUTILS: Lua Utility Functions for Lightroom', 387 | font = '', 388 | }, 389 | viewFactory:edit_field { 390 | width_in_chars = 80, 391 | height_in_lines = 5, 392 | enabled = false, 393 | value = LutilsAttribution 394 | } 395 | } 396 | } 397 | end 398 | 399 | 400 | return ClarifaiTaggerInfoProvider 401 | -------------------------------------------------------------------------------- /ClarifaiTagger.lrdevplugin/JSON.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | -- 3 | -- Simple JSON encoding and decoding in pure Lua. 4 | -- 5 | -- Copyright 2010-2016 Jeffrey Friedl 6 | -- http://regex.info/blog/ 7 | -- Latest version: http://regex.info/blog/lua/json 8 | -- 9 | -- This code is released under a Creative Commons CC-BY "Attribution" License: 10 | -- http://creativecommons.org/licenses/by/3.0/deed.en_US 11 | -- 12 | -- It can be used for any purpose so long as: 13 | -- 1) the copyright notice above is maintained 14 | -- 2) the web-page links above are maintained 15 | -- 3) the 'AUTHOR_NOTE' string below is maintained 16 | -- 17 | local VERSION = '20161109.21' -- version history at end of file 18 | local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20161109.21 ]-" 19 | 20 | -- 21 | -- The 'AUTHOR_NOTE' variable exists so that information about the source 22 | -- of the package is maintained even in compiled versions. It's also 23 | -- included in OBJDEF below mostly to quiet warnings about unused variables. 24 | -- 25 | local OBJDEF = { 26 | VERSION = VERSION, 27 | AUTHOR_NOTE = AUTHOR_NOTE, 28 | } 29 | 30 | 31 | -- 32 | -- Simple JSON encoding and decoding in pure Lua. 33 | -- JSON definition: http://www.json.org/ 34 | -- 35 | -- 36 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 37 | -- 38 | -- local lua_value = JSON:decode(raw_json_text) 39 | -- 40 | -- local raw_json_text = JSON:encode(lua_table_or_value) 41 | -- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability 42 | -- 43 | -- 44 | -- 45 | -- DECODING (from a JSON string to a Lua table) 46 | -- 47 | -- 48 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 49 | -- 50 | -- local lua_value = JSON:decode(raw_json_text) 51 | -- 52 | -- If the JSON text is for an object or an array, e.g. 53 | -- { "what": "books", "count": 3 } 54 | -- or 55 | -- [ "Larry", "Curly", "Moe" ] 56 | -- 57 | -- the result is a Lua table, e.g. 58 | -- { what = "books", count = 3 } 59 | -- or 60 | -- { "Larry", "Curly", "Moe" } 61 | -- 62 | -- 63 | -- The encode and decode routines accept an optional second argument, 64 | -- "etc", which is not used during encoding or decoding, but upon error 65 | -- is passed along to error handlers. It can be of any type (including nil). 66 | -- 67 | -- 68 | -- 69 | -- ERROR HANDLING 70 | -- 71 | -- With most errors during decoding, this code calls 72 | -- 73 | -- JSON:onDecodeError(message, text, location, etc) 74 | -- 75 | -- with a message about the error, and if known, the JSON text being 76 | -- parsed and the byte count where the problem was discovered. You can 77 | -- replace the default JSON:onDecodeError() with your own function. 78 | -- 79 | -- The default onDecodeError() merely augments the message with data 80 | -- about the text and the location if known (and if a second 'etc' 81 | -- argument had been provided to decode(), its value is tacked onto the 82 | -- message as well), and then calls JSON.assert(), which itself defaults 83 | -- to Lua's built-in assert(), and can also be overridden. 84 | -- 85 | -- For example, in an Adobe Lightroom plugin, you might use something like 86 | -- 87 | -- function JSON:onDecodeError(message, text, location, etc) 88 | -- LrErrors.throwUserError("Internal Error: invalid JSON data") 89 | -- end 90 | -- 91 | -- or even just 92 | -- 93 | -- function JSON.assert(message) 94 | -- LrErrors.throwUserError("Internal Error: " .. message) 95 | -- end 96 | -- 97 | -- If JSON:decode() is passed a nil, this is called instead: 98 | -- 99 | -- JSON:onDecodeOfNilError(message, nil, nil, etc) 100 | -- 101 | -- and if JSON:decode() is passed HTML instead of JSON, this is called: 102 | -- 103 | -- JSON:onDecodeOfHTMLError(message, text, nil, etc) 104 | -- 105 | -- The use of the fourth 'etc' argument allows stronger coordination 106 | -- between decoding and error reporting, especially when you provide your 107 | -- own error-handling routines. Continuing with the the Adobe Lightroom 108 | -- plugin example: 109 | -- 110 | -- function JSON:onDecodeError(message, text, location, etc) 111 | -- local note = "Internal Error: invalid JSON data" 112 | -- if type(etc) = 'table' and etc.photo then 113 | -- note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName') 114 | -- end 115 | -- LrErrors.throwUserError(note) 116 | -- end 117 | -- 118 | -- : 119 | -- : 120 | -- 121 | -- for i, photo in ipairs(photosToProcess) do 122 | -- : 123 | -- : 124 | -- local data = JSON:decode(someJsonText, { photo = photo }) 125 | -- : 126 | -- : 127 | -- end 128 | -- 129 | -- 130 | -- 131 | -- If the JSON text passed to decode() has trailing garbage (e.g. as with the JSON "[123]xyzzy"), 132 | -- the method 133 | -- 134 | -- JSON:onTrailingGarbage(json_text, location, parsed_value, etc) 135 | -- 136 | -- is invoked, where: 137 | -- 138 | -- json_text is the original JSON text being parsed, 139 | -- location is the count of bytes into json_text where the garbage starts (6 in the example), 140 | -- parsed_value is the Lua result of what was successfully parsed ({123} in the example), 141 | -- etc is as above. 142 | -- 143 | -- If JSON:onTrailingGarbage() does not abort, it should return the value decode() should return, 144 | -- or nil + an error message. 145 | -- 146 | -- local new_value, error_message = JSON:onTrailingGarbage() 147 | -- 148 | -- The default handler just invokes JSON:onDecodeError("trailing garbage"...), but you can have 149 | -- this package ignore trailing garbage via 150 | -- 151 | -- function JSON:onTrailingGarbage(json_text, location, parsed_value, etc) 152 | -- return parsed_value 153 | -- end 154 | -- 155 | -- 156 | -- DECODING AND STRICT TYPES 157 | -- 158 | -- Because both JSON objects and JSON arrays are converted to Lua tables, 159 | -- it's not normally possible to tell which original JSON type a 160 | -- particular Lua table was derived from, or guarantee decode-encode 161 | -- round-trip equivalency. 162 | -- 163 | -- However, if you enable strictTypes, e.g. 164 | -- 165 | -- JSON = assert(loadfile "JSON.lua")() --load the routines 166 | -- JSON.strictTypes = true 167 | -- 168 | -- then the Lua table resulting from the decoding of a JSON object or 169 | -- JSON array is marked via Lua metatable, so that when re-encoded with 170 | -- JSON:encode() it ends up as the appropriate JSON type. 171 | -- 172 | -- (This is not the default because other routines may not work well with 173 | -- tables that have a metatable set, for example, Lightroom API calls.) 174 | -- 175 | -- 176 | -- ENCODING (from a lua table to a JSON string) 177 | -- 178 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 179 | -- 180 | -- local raw_json_text = JSON:encode(lua_table_or_value) 181 | -- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability 182 | -- local custom_pretty = JSON:encode(lua_table_or_value, etc, { pretty = true, indent = "| ", align_keys = false }) 183 | -- 184 | -- On error during encoding, this code calls: 185 | -- 186 | -- JSON:onEncodeError(message, etc) 187 | -- 188 | -- which you can override in your local JSON object. 189 | -- 190 | -- The 'etc' in the error call is the second argument to encode() 191 | -- and encode_pretty(), or nil if it wasn't provided. 192 | -- 193 | -- 194 | -- ENCODING OPTIONS 195 | -- 196 | -- An optional third argument, a table of options, can be provided to encode(). 197 | -- 198 | -- encode_options = { 199 | -- -- options for making "pretty" human-readable JSON (see "PRETTY-PRINTING" below) 200 | -- pretty = true, 201 | -- indent = " ", 202 | -- align_keys = false, 203 | -- 204 | -- -- other output-related options 205 | -- null = "\0", -- see "ENCODING JSON NULL VALUES" below 206 | -- stringsAreUtf8 = false, -- see "HANDLING UNICODE LINE AND PARAGRAPH SEPARATORS FOR JAVA" below 207 | -- } 208 | -- 209 | -- json_string = JSON:encode(mytable, etc, encode_options) 210 | -- 211 | -- 212 | -- 213 | -- For reference, the defaults are: 214 | -- 215 | -- pretty = false 216 | -- null = nil, 217 | -- stringsAreUtf8 = false, 218 | -- 219 | -- 220 | -- 221 | -- PRETTY-PRINTING 222 | -- 223 | -- Enabling the 'pretty' encode option helps generate human-readable JSON. 224 | -- 225 | -- pretty = JSON:encode(val, etc, { 226 | -- pretty = true, 227 | -- indent = " ", 228 | -- align_keys = false, 229 | -- }) 230 | -- 231 | -- encode_pretty() is also provided: it's identical to encode() except 232 | -- that encode_pretty() provides a default options table if none given in the call: 233 | -- 234 | -- { pretty = true, align_keys = false, indent = " " } 235 | -- 236 | -- For example, if 237 | -- 238 | -- JSON:encode(data) 239 | -- 240 | -- produces: 241 | -- 242 | -- {"city":"Kyoto","climate":{"avg_temp":16,"humidity":"high","snowfall":"minimal"},"country":"Japan","wards":11} 243 | -- 244 | -- then 245 | -- 246 | -- JSON:encode_pretty(data) 247 | -- 248 | -- produces: 249 | -- 250 | -- { 251 | -- "city": "Kyoto", 252 | -- "climate": { 253 | -- "avg_temp": 16, 254 | -- "humidity": "high", 255 | -- "snowfall": "minimal" 256 | -- }, 257 | -- "country": "Japan", 258 | -- "wards": 11 259 | -- } 260 | -- 261 | -- The following three lines return identical results: 262 | -- JSON:encode_pretty(data) 263 | -- JSON:encode_pretty(data, nil, { pretty = true, align_keys = false, indent = " " }) 264 | -- JSON:encode (data, nil, { pretty = true, align_keys = false, indent = " " }) 265 | -- 266 | -- An example of setting your own indent string: 267 | -- 268 | -- JSON:encode_pretty(data, nil, { pretty = true, indent = "| " }) 269 | -- 270 | -- produces: 271 | -- 272 | -- { 273 | -- | "city": "Kyoto", 274 | -- | "climate": { 275 | -- | | "avg_temp": 16, 276 | -- | | "humidity": "high", 277 | -- | | "snowfall": "minimal" 278 | -- | }, 279 | -- | "country": "Japan", 280 | -- | "wards": 11 281 | -- } 282 | -- 283 | -- An example of setting align_keys to true: 284 | -- 285 | -- JSON:encode_pretty(data, nil, { pretty = true, indent = " ", align_keys = true }) 286 | -- 287 | -- produces: 288 | -- 289 | -- { 290 | -- "city": "Kyoto", 291 | -- "climate": { 292 | -- "avg_temp": 16, 293 | -- "humidity": "high", 294 | -- "snowfall": "minimal" 295 | -- }, 296 | -- "country": "Japan", 297 | -- "wards": 11 298 | -- } 299 | -- 300 | -- which I must admit is kinda ugly, sorry. This was the default for 301 | -- encode_pretty() prior to version 20141223.14. 302 | -- 303 | -- 304 | -- HANDLING UNICODE LINE AND PARAGRAPH SEPARATORS FOR JAVA 305 | -- 306 | -- If the 'stringsAreUtf8' encode option is set to true, consider Lua strings not as a sequence of bytes, 307 | -- but as a sequence of UTF-8 characters. 308 | -- 309 | -- Currently, the only practical effect of setting this option is that Unicode LINE and PARAGRAPH 310 | -- separators, if found in a string, are encoded with a JSON escape instead of being dumped as is. 311 | -- The JSON is valid either way, but encoding this way, apparently, allows the resulting JSON 312 | -- to also be valid Java. 313 | -- 314 | -- AMBIGUOUS SITUATIONS DURING THE ENCODING 315 | -- 316 | -- During the encode, if a Lua table being encoded contains both string 317 | -- and numeric keys, it fits neither JSON's idea of an object, nor its 318 | -- idea of an array. To get around this, when any string key exists (or 319 | -- when non-positive numeric keys exist), numeric keys are converted to 320 | -- strings. 321 | -- 322 | -- For example, 323 | -- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) 324 | -- produces the JSON object 325 | -- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} 326 | -- 327 | -- To prohibit this conversion and instead make it an error condition, set 328 | -- JSON.noKeyConversion = true 329 | -- 330 | -- 331 | -- ENCODING JSON NULL VALUES 332 | -- 333 | -- Lua tables completely omit keys whose value is nil, so without special handling there's 334 | -- no way to get a field in a JSON object with a null value. For example 335 | -- JSON:encode({ username = "admin", password = nil }) 336 | -- produces 337 | -- {"username":"admin"} 338 | -- 339 | -- In order to actually produce 340 | -- {"username":"admin", "password":null} 341 | -- one can include a string value for a "null" field in the options table passed to encode().... 342 | -- any Lua table entry with that value becomes null in the JSON output: 343 | -- JSON:encode({ username = "admin", password = "xyzzy" }, nil, { null = "xyzzy" }) 344 | -- produces 345 | -- {"username":"admin", "password":null} 346 | -- 347 | -- Just be sure to use a string that is otherwise unlikely to appear in your data. 348 | -- The string "\0" (a string with one null byte) may well be appropriate for many applications. 349 | -- 350 | -- The "null" options also applies to Lua tables that become JSON arrays. 351 | -- JSON:encode({ "one", "two", nil, nil }) 352 | -- produces 353 | -- ["one","two"] 354 | -- while 355 | -- NULL = "\0" 356 | -- JSON:encode({ "one", "two", NULL, NULL}, nil, { null = NULL }) 357 | -- produces 358 | -- ["one","two",null,null] 359 | -- 360 | -- 361 | -- 362 | -- 363 | -- HANDLING LARGE AND/OR PRECISE NUMBERS 364 | -- 365 | -- 366 | -- Without special handling, numbers in JSON can lose precision in Lua. 367 | -- For example: 368 | -- 369 | -- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') 370 | -- 371 | -- print("small: ", type(T.small), T.small) 372 | -- print("big: ", type(T.big), T.big) 373 | -- print("precise: ", type(T.precise), T.precise) 374 | -- 375 | -- produces 376 | -- 377 | -- small: number 12345 378 | -- big: number 1.2345678901235e+28 379 | -- precise: number 9876.6789012346 380 | -- 381 | -- Precision is lost with both 'big' and 'precise'. 382 | -- 383 | -- This package offers ways to try to handle this better (for some definitions of "better")... 384 | -- 385 | -- The most precise method is by setting the global: 386 | -- 387 | -- JSON.decodeNumbersAsObjects = true 388 | -- 389 | -- When this is set, numeric JSON data is encoded into Lua in a form that preserves the exact 390 | -- JSON numeric presentation when re-encoded back out to JSON, or accessed in Lua as a string. 391 | -- 392 | -- (This is done by encoding the numeric data with a Lua table/metatable that returns 393 | -- the possibly-imprecise numeric form when accessed numerically, but the original precise 394 | -- representation when accessed as a string. You can also explicitly access 395 | -- via JSON:forceString() and JSON:forceNumber()) 396 | -- 397 | -- Consider the example above, with this option turned on: 398 | -- 399 | -- JSON.decodeNumbersAsObjects = true 400 | -- 401 | -- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') 402 | -- 403 | -- print("small: ", type(T.small), T.small) 404 | -- print("big: ", type(T.big), T.big) 405 | -- print("precise: ", type(T.precise), T.precise) 406 | -- 407 | -- This now produces: 408 | -- 409 | -- small: table 12345 410 | -- big: table 12345678901234567890123456789 411 | -- precise: table 9876.67890123456789012345 412 | -- 413 | -- However, within Lua you can still use the values (e.g. T.precise in the example above) in numeric 414 | -- contexts. In such cases you'll get the possibly-imprecise numeric version, but in string contexts 415 | -- and when the data finds its way to this package's encode() function, the original full-precision 416 | -- representation is used. 417 | -- 418 | -- Even without using the JSON.decodeNumbersAsObjects option, you can encode numbers 419 | -- in your Lua table that retain high precision upon encoding to JSON, by using the JSON:asNumber() 420 | -- function: 421 | -- 422 | -- T = { 423 | -- imprecise = 123456789123456789.123456789123456789, 424 | -- precise = JSON:asNumber("123456789123456789.123456789123456789") 425 | -- } 426 | -- 427 | -- print(JSON:encode_pretty(T)) 428 | -- 429 | -- This produces: 430 | -- 431 | -- { 432 | -- "precise": 123456789123456789.123456789123456789, 433 | -- "imprecise": 1.2345678912346e+17 434 | -- } 435 | -- 436 | -- 437 | -- 438 | -- A different way to handle big/precise JSON numbers is to have decode() merely return 439 | -- the exact string representation of the number instead of the number itself. 440 | -- This approach might be useful when the numbers are merely some kind of opaque 441 | -- object identifier and you want to work with them in Lua as strings anyway. 442 | -- 443 | -- This approach is enabled by setting 444 | -- 445 | -- JSON.decodeIntegerStringificationLength = 10 446 | -- 447 | -- The value is the number of digits (of the integer part of the number) at which to stringify numbers. 448 | -- 449 | -- Consider our previous example with this option set to 10: 450 | -- 451 | -- JSON.decodeIntegerStringificationLength = 10 452 | -- 453 | -- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') 454 | -- 455 | -- print("small: ", type(T.small), T.small) 456 | -- print("big: ", type(T.big), T.big) 457 | -- print("precise: ", type(T.precise), T.precise) 458 | -- 459 | -- This produces: 460 | -- 461 | -- small: number 12345 462 | -- big: string 12345678901234567890123456789 463 | -- precise: number 9876.6789012346 464 | -- 465 | -- The long integer of the 'big' field is at least JSON.decodeIntegerStringificationLength digits 466 | -- in length, so it's converted not to a Lua integer but to a Lua string. Using a value of 0 or 1 ensures 467 | -- that all JSON numeric data becomes strings in Lua. 468 | -- 469 | -- Note that unlike 470 | -- JSON.decodeNumbersAsObjects = true 471 | -- this stringification is simple and unintelligent: the JSON number simply becomes a Lua string, and that's the end of it. 472 | -- If the string is then converted back to JSON, it's still a string. After running the code above, adding 473 | -- print(JSON:encode(T)) 474 | -- produces 475 | -- {"big":"12345678901234567890123456789","precise":9876.6789012346,"small":12345} 476 | -- which is unlikely to be desired. 477 | -- 478 | -- There's a comparable option for the length of the decimal part of a number: 479 | -- 480 | -- JSON.decodeDecimalStringificationLength 481 | -- 482 | -- This can be used alone or in conjunction with 483 | -- 484 | -- JSON.decodeIntegerStringificationLength 485 | -- 486 | -- to trip stringification on precise numbers with at least JSON.decodeIntegerStringificationLength digits after 487 | -- the decimal point. 488 | -- 489 | -- This example: 490 | -- 491 | -- JSON.decodeIntegerStringificationLength = 10 492 | -- JSON.decodeDecimalStringificationLength = 5 493 | -- 494 | -- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') 495 | -- 496 | -- print("small: ", type(T.small), T.small) 497 | -- print("big: ", type(T.big), T.big) 498 | -- print("precise: ", type(T.precise), T.precise) 499 | -- 500 | -- produces: 501 | -- 502 | -- small: number 12345 503 | -- big: string 12345678901234567890123456789 504 | -- precise: string 9876.67890123456789012345 505 | -- 506 | -- 507 | -- 508 | -- 509 | -- 510 | -- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT 511 | -- 512 | -- assert 513 | -- onDecodeError 514 | -- onDecodeOfNilError 515 | -- onDecodeOfHTMLError 516 | -- onTrailingGarbage 517 | -- onEncodeError 518 | -- 519 | -- If you want to create a separate Lua JSON object with its own error handlers, 520 | -- you can reload JSON.lua or use the :new() method. 521 | -- 522 | --------------------------------------------------------------------------- 523 | 524 | local default_pretty_indent = " " 525 | local default_pretty_options = { pretty = true, align_keys = false, indent = default_pretty_indent } 526 | 527 | local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray 528 | local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject 529 | 530 | function OBJDEF:newArray(tbl) 531 | return setmetatable(tbl or {}, isArray) 532 | end 533 | 534 | function OBJDEF:newObject(tbl) 535 | return setmetatable(tbl or {}, isObject) 536 | end 537 | 538 | 539 | 540 | 541 | local function getnum(op) 542 | return type(op) == 'number' and op or op.N 543 | end 544 | 545 | local isNumber = { 546 | __tostring = function(T) return T.S end, 547 | __unm = function(op) return getnum(op) end, 548 | 549 | __concat = function(op1, op2) return tostring(op1) .. tostring(op2) end, 550 | __add = function(op1, op2) return getnum(op1) + getnum(op2) end, 551 | __sub = function(op1, op2) return getnum(op1) - getnum(op2) end, 552 | __mul = function(op1, op2) return getnum(op1) * getnum(op2) end, 553 | __div = function(op1, op2) return getnum(op1) / getnum(op2) end, 554 | __mod = function(op1, op2) return getnum(op1) % getnum(op2) end, 555 | __pow = function(op1, op2) return getnum(op1) ^ getnum(op2) end, 556 | __lt = function(op1, op2) return getnum(op1) < getnum(op2) end, 557 | __eq = function(op1, op2) return getnum(op1) == getnum(op2) end, 558 | __le = function(op1, op2) return getnum(op1) <= getnum(op2) end, 559 | } 560 | isNumber.__index = isNumber 561 | 562 | function OBJDEF:asNumber(item) 563 | 564 | if getmetatable(item) == isNumber then 565 | -- it's already a JSON number object. 566 | return item 567 | elseif type(item) == 'table' and type(item.S) == 'string' and type(item.N) == 'number' then 568 | -- it's a number-object table that lost its metatable, so give it one 569 | return setmetatable(item, isNumber) 570 | else 571 | -- the normal situation... given a number or a string representation of a number.... 572 | local holder = { 573 | S = tostring(item), -- S is the representation of the number as a string, which remains precise 574 | N = tonumber(item), -- N is the number as a Lua number. 575 | } 576 | return setmetatable(holder, isNumber) 577 | end 578 | end 579 | 580 | -- 581 | -- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above, 582 | -- return the string version. This shouldn't be needed often because the 'isNumber' object should autoconvert 583 | -- to a string in most cases, but it's here to allow it to be forced when needed. 584 | -- 585 | function OBJDEF:forceString(item) 586 | if type(item) == 'table' and type(item.S) == 'string' then 587 | return item.S 588 | else 589 | return tostring(item) 590 | end 591 | end 592 | 593 | -- 594 | -- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above, 595 | -- return the numeric version. 596 | -- 597 | function OBJDEF:forceNumber(item) 598 | if type(item) == 'table' and type(item.N) == 'number' then 599 | return item.N 600 | else 601 | return tonumber(item) 602 | end 603 | end 604 | 605 | 606 | local function unicode_codepoint_as_utf8(codepoint) 607 | -- 608 | -- codepoint is a number 609 | -- 610 | if codepoint <= 127 then 611 | return string.char(codepoint) 612 | 613 | elseif codepoint <= 2047 then 614 | -- 615 | -- 110yyyxx 10xxxxxx <-- useful notation from http://en.wikipedia.org/wiki/Utf8 616 | -- 617 | local highpart = math.floor(codepoint / 0x40) 618 | local lowpart = codepoint - (0x40 * highpart) 619 | return string.char(0xC0 + highpart, 620 | 0x80 + lowpart) 621 | 622 | elseif codepoint <= 65535 then 623 | -- 624 | -- 1110yyyy 10yyyyxx 10xxxxxx 625 | -- 626 | local highpart = math.floor(codepoint / 0x1000) 627 | local remainder = codepoint - 0x1000 * highpart 628 | local midpart = math.floor(remainder / 0x40) 629 | local lowpart = remainder - 0x40 * midpart 630 | 631 | highpart = 0xE0 + highpart 632 | midpart = 0x80 + midpart 633 | lowpart = 0x80 + lowpart 634 | 635 | -- 636 | -- Check for an invalid character (thanks Andy R. at Adobe). 637 | -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070 638 | -- 639 | if ( highpart == 0xE0 and midpart < 0xA0 ) or 640 | ( highpart == 0xED and midpart > 0x9F ) or 641 | ( highpart == 0xF0 and midpart < 0x90 ) or 642 | ( highpart == 0xF4 and midpart > 0x8F ) 643 | then 644 | return "?" 645 | else 646 | return string.char(highpart, 647 | midpart, 648 | lowpart) 649 | end 650 | 651 | else 652 | -- 653 | -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx 654 | -- 655 | local highpart = math.floor(codepoint / 0x40000) 656 | local remainder = codepoint - 0x40000 * highpart 657 | local midA = math.floor(remainder / 0x1000) 658 | remainder = remainder - 0x1000 * midA 659 | local midB = math.floor(remainder / 0x40) 660 | local lowpart = remainder - 0x40 * midB 661 | 662 | return string.char(0xF0 + highpart, 663 | 0x80 + midA, 664 | 0x80 + midB, 665 | 0x80 + lowpart) 666 | end 667 | end 668 | 669 | function OBJDEF:onDecodeError(message, text, location, etc) 670 | if text then 671 | if location then 672 | message = string.format("%s at byte %d of: %s", message, location, text) 673 | else 674 | message = string.format("%s: %s", message, text) 675 | end 676 | end 677 | 678 | if etc ~= nil then 679 | message = message .. " (" .. OBJDEF:encode(etc) .. ")" 680 | end 681 | 682 | if self.assert then 683 | self.assert(false, message) 684 | else 685 | assert(false, message) 686 | end 687 | end 688 | 689 | function OBJDEF:onTrailingGarbage(json_text, location, parsed_value, etc) 690 | return self:onDecodeError("trailing garbage", json_text, location, etc) 691 | end 692 | 693 | OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeError 694 | OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError 695 | 696 | function OBJDEF:onEncodeError(message, etc) 697 | if etc ~= nil then 698 | message = message .. " (" .. OBJDEF:encode(etc) .. ")" 699 | end 700 | 701 | if self.assert then 702 | self.assert(false, message) 703 | else 704 | assert(false, message) 705 | end 706 | end 707 | 708 | local function grok_number(self, text, start, options) 709 | -- 710 | -- Grab the integer part 711 | -- 712 | local integer_part = text:match('^-?[1-9]%d*', start) 713 | or text:match("^-?0", start) 714 | 715 | if not integer_part then 716 | self:onDecodeError("expected number", text, start, options.etc) 717 | return nil, start -- in case the error method doesn't abort, return something sensible 718 | end 719 | 720 | local i = start + integer_part:len() 721 | 722 | -- 723 | -- Grab an optional decimal part 724 | -- 725 | local decimal_part = text:match('^%.%d+', i) or "" 726 | 727 | i = i + decimal_part:len() 728 | 729 | -- 730 | -- Grab an optional exponential part 731 | -- 732 | local exponent_part = text:match('^[eE][-+]?%d+', i) or "" 733 | 734 | i = i + exponent_part:len() 735 | 736 | local full_number_text = integer_part .. decimal_part .. exponent_part 737 | 738 | if options.decodeNumbersAsObjects then 739 | return OBJDEF:asNumber(full_number_text), i 740 | end 741 | 742 | -- 743 | -- If we're told to stringify under certain conditions, so do. 744 | -- We punt a bit when there's an exponent by just stringifying no matter what. 745 | -- I suppose we should really look to see whether the exponent is actually big enough one 746 | -- way or the other to trip stringification, but I'll be lazy about it until someone asks. 747 | -- 748 | if (options.decodeIntegerStringificationLength 749 | and 750 | (integer_part:len() >= options.decodeIntegerStringificationLength or exponent_part:len() > 0)) 751 | 752 | or 753 | 754 | (options.decodeDecimalStringificationLength 755 | and 756 | (decimal_part:len() >= options.decodeDecimalStringificationLength or exponent_part:len() > 0)) 757 | then 758 | return full_number_text, i -- this returns the exact string representation seen in the original JSON 759 | end 760 | 761 | 762 | 763 | local as_number = tonumber(full_number_text) 764 | 765 | if not as_number then 766 | self:onDecodeError("bad number", text, start, options.etc) 767 | return nil, start -- in case the error method doesn't abort, return something sensible 768 | end 769 | 770 | return as_number, i 771 | end 772 | 773 | 774 | local function grok_string(self, text, start, options) 775 | 776 | if text:sub(start,start) ~= '"' then 777 | self:onDecodeError("expected string's opening quote", text, start, options.etc) 778 | return nil, start -- in case the error method doesn't abort, return something sensible 779 | end 780 | 781 | local i = start + 1 -- +1 to bypass the initial quote 782 | local text_len = text:len() 783 | local VALUE = "" 784 | while i <= text_len do 785 | local c = text:sub(i,i) 786 | if c == '"' then 787 | return VALUE, i + 1 788 | end 789 | if c ~= '\\' then 790 | VALUE = VALUE .. c 791 | i = i + 1 792 | elseif text:match('^\\b', i) then 793 | VALUE = VALUE .. "\b" 794 | i = i + 2 795 | elseif text:match('^\\f', i) then 796 | VALUE = VALUE .. "\f" 797 | i = i + 2 798 | elseif text:match('^\\n', i) then 799 | VALUE = VALUE .. "\n" 800 | i = i + 2 801 | elseif text:match('^\\r', i) then 802 | VALUE = VALUE .. "\r" 803 | i = i + 2 804 | elseif text:match('^\\t', i) then 805 | VALUE = VALUE .. "\t" 806 | i = i + 2 807 | else 808 | local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) 809 | if hex then 810 | i = i + 6 -- bypass what we just read 811 | 812 | -- We have a Unicode codepoint. It could be standalone, or if in the proper range and 813 | -- followed by another in a specific range, it'll be a two-code surrogate pair. 814 | local codepoint = tonumber(hex, 16) 815 | if codepoint >= 0xD800 and codepoint <= 0xDBFF then 816 | -- it's a hi surrogate... see whether we have a following low 817 | local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) 818 | if lo_surrogate then 819 | i = i + 6 -- bypass the low surrogate we just read 820 | codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16) 821 | else 822 | -- not a proper low, so we'll just leave the first codepoint as is and spit it out. 823 | end 824 | end 825 | VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint) 826 | 827 | else 828 | 829 | -- just pass through what's escaped 830 | VALUE = VALUE .. text:match('^\\(.)', i) 831 | i = i + 2 832 | end 833 | end 834 | end 835 | 836 | self:onDecodeError("unclosed string", text, start, options.etc) 837 | return nil, start -- in case the error method doesn't abort, return something sensible 838 | end 839 | 840 | local function skip_whitespace(text, start) 841 | 842 | local _, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2 843 | if match_end then 844 | return match_end + 1 845 | else 846 | return start 847 | end 848 | end 849 | 850 | local grok_one -- assigned later 851 | 852 | local function grok_object(self, text, start, options) 853 | 854 | if text:sub(start,start) ~= '{' then 855 | self:onDecodeError("expected '{'", text, start, options.etc) 856 | return nil, start -- in case the error method doesn't abort, return something sensible 857 | end 858 | 859 | local i = skip_whitespace(text, start + 1) -- +1 to skip the '{' 860 | 861 | local VALUE = self.strictTypes and self:newObject { } or { } 862 | 863 | if text:sub(i,i) == '}' then 864 | return VALUE, i + 1 865 | end 866 | local text_len = text:len() 867 | while i <= text_len do 868 | local key, new_i = grok_string(self, text, i, options) 869 | 870 | i = skip_whitespace(text, new_i) 871 | 872 | if text:sub(i, i) ~= ':' then 873 | self:onDecodeError("expected colon", text, i, options.etc) 874 | return nil, i -- in case the error method doesn't abort, return something sensible 875 | end 876 | 877 | i = skip_whitespace(text, i + 1) 878 | 879 | local new_val, new_i = grok_one(self, text, i, options) 880 | 881 | VALUE[key] = new_val 882 | 883 | -- 884 | -- Expect now either '}' to end things, or a ',' to allow us to continue. 885 | -- 886 | i = skip_whitespace(text, new_i) 887 | 888 | local c = text:sub(i,i) 889 | 890 | if c == '}' then 891 | return VALUE, i + 1 892 | end 893 | 894 | if text:sub(i, i) ~= ',' then 895 | self:onDecodeError("expected comma or '}'", text, i, options.etc) 896 | return nil, i -- in case the error method doesn't abort, return something sensible 897 | end 898 | 899 | i = skip_whitespace(text, i + 1) 900 | end 901 | 902 | self:onDecodeError("unclosed '{'", text, start, options.etc) 903 | return nil, start -- in case the error method doesn't abort, return something sensible 904 | end 905 | 906 | local function grok_array(self, text, start, options) 907 | if text:sub(start,start) ~= '[' then 908 | self:onDecodeError("expected '['", text, start, options.etc) 909 | return nil, start -- in case the error method doesn't abort, return something sensible 910 | end 911 | 912 | local i = skip_whitespace(text, start + 1) -- +1 to skip the '[' 913 | local VALUE = self.strictTypes and self:newArray { } or { } 914 | if text:sub(i,i) == ']' then 915 | return VALUE, i + 1 916 | end 917 | 918 | local VALUE_INDEX = 1 919 | 920 | local text_len = text:len() 921 | while i <= text_len do 922 | local val, new_i = grok_one(self, text, i, options) 923 | 924 | -- can't table.insert(VALUE, val) here because it's a no-op if val is nil 925 | VALUE[VALUE_INDEX] = val 926 | VALUE_INDEX = VALUE_INDEX + 1 927 | 928 | i = skip_whitespace(text, new_i) 929 | 930 | -- 931 | -- Expect now either ']' to end things, or a ',' to allow us to continue. 932 | -- 933 | local c = text:sub(i,i) 934 | if c == ']' then 935 | return VALUE, i + 1 936 | end 937 | if text:sub(i, i) ~= ',' then 938 | self:onDecodeError("expected comma or ']'", text, i, options.etc) 939 | return nil, i -- in case the error method doesn't abort, return something sensible 940 | end 941 | i = skip_whitespace(text, i + 1) 942 | end 943 | self:onDecodeError("unclosed '['", text, start, options.etc) 944 | return nil, i -- in case the error method doesn't abort, return something sensible 945 | end 946 | 947 | 948 | grok_one = function(self, text, start, options) 949 | -- Skip any whitespace 950 | start = skip_whitespace(text, start) 951 | 952 | if start > text:len() then 953 | self:onDecodeError("unexpected end of string", text, nil, options.etc) 954 | return nil, start -- in case the error method doesn't abort, return something sensible 955 | end 956 | 957 | if text:find('^"', start) then 958 | return grok_string(self, text, start, options.etc) 959 | 960 | elseif text:find('^[-0123456789 ]', start) then 961 | return grok_number(self, text, start, options) 962 | 963 | elseif text:find('^%{', start) then 964 | return grok_object(self, text, start, options) 965 | 966 | elseif text:find('^%[', start) then 967 | return grok_array(self, text, start, options) 968 | 969 | elseif text:find('^true', start) then 970 | return true, start + 4 971 | 972 | elseif text:find('^false', start) then 973 | return false, start + 5 974 | 975 | elseif text:find('^null', start) then 976 | return nil, start + 4 977 | 978 | else 979 | self:onDecodeError("can't parse JSON", text, start, options.etc) 980 | return nil, 1 -- in case the error method doesn't abort, return something sensible 981 | end 982 | end 983 | 984 | function OBJDEF:decode(text, etc, options) 985 | -- 986 | -- If the user didn't pass in a table of decode options, make an empty one. 987 | -- 988 | if type(options) ~= 'table' then 989 | options = {} 990 | end 991 | 992 | -- 993 | -- If they passed in an 'etc' argument, stuff it into the options. 994 | -- (If not, any 'etc' field in the options they passed in remains to be used) 995 | -- 996 | if etc ~= nil then 997 | options.etc = etc 998 | end 999 | 1000 | 1001 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 1002 | local error_message = "JSON:decode must be called in method format" 1003 | OBJDEF:onDecodeError(error_message, nil, nil, options.etc) 1004 | return nil, error_message -- in case the error method doesn't abort, return something sensible 1005 | end 1006 | 1007 | if text == nil then 1008 | local error_message = "nil passed to JSON:decode()" 1009 | self:onDecodeOfNilError(error_message, nil, nil, options.etc) 1010 | return nil, error_message -- in case the error method doesn't abort, return something sensible 1011 | 1012 | elseif type(text) ~= 'string' then 1013 | local error_message = "expected string argument to JSON:decode()" 1014 | self:onDecodeError(string.format("%s, got %s", error_message, type(text)), nil, nil, options.etc) 1015 | return nil, error_message -- in case the error method doesn't abort, return something sensible 1016 | end 1017 | 1018 | if text:match('^%s*$') then 1019 | -- an empty string is nothing, but not an error 1020 | return nil 1021 | end 1022 | 1023 | if text:match('^%s*<') then 1024 | -- Can't be JSON... we'll assume it's HTML 1025 | local error_message = "HTML passed to JSON:decode()" 1026 | self:onDecodeOfHTMLError(error_message, text, nil, options.etc) 1027 | return nil, error_message -- in case the error method doesn't abort, return something sensible 1028 | end 1029 | 1030 | -- 1031 | -- Ensure that it's not UTF-32 or UTF-16. 1032 | -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3), 1033 | -- but this package can't handle them. 1034 | -- 1035 | if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then 1036 | local error_message = "JSON package groks only UTF-8, sorry" 1037 | self:onDecodeError(error_message, text, nil, options.etc) 1038 | return nil, error_message -- in case the error method doesn't abort, return something sensible 1039 | end 1040 | 1041 | -- 1042 | -- apply global options 1043 | -- 1044 | if options.decodeNumbersAsObjects == nil then 1045 | options.decodeNumbersAsObjects = self.decodeNumbersAsObjects 1046 | end 1047 | if options.decodeIntegerStringificationLength == nil then 1048 | options.decodeIntegerStringificationLength = self.decodeIntegerStringificationLength 1049 | end 1050 | if options.decodeDecimalStringificationLength == nil then 1051 | options.decodeDecimalStringificationLength = self.decodeDecimalStringificationLength 1052 | end 1053 | 1054 | -- 1055 | -- Finally, go parse it 1056 | -- 1057 | local success, value, next_i = pcall(grok_one, self, text, 1, options) 1058 | 1059 | if success then 1060 | 1061 | local error_message = nil 1062 | if next_i ~= #text + 1 then 1063 | -- something's left over after we parsed the first thing.... whitespace is allowed. 1064 | next_i = skip_whitespace(text, next_i) 1065 | 1066 | -- if we have something left over now, it's trailing garbage 1067 | if next_i ~= #text + 1 then 1068 | value, error_message = self:onTrailingGarbage(text, next_i, value, options.etc) 1069 | end 1070 | end 1071 | return value, error_message 1072 | 1073 | else 1074 | 1075 | -- If JSON:onDecodeError() didn't abort out of the pcall, we'll have received 1076 | -- the error message here as "value", so pass it along as an assert. 1077 | local error_message = value 1078 | if self.assert then 1079 | self.assert(false, error_message) 1080 | else 1081 | assert(false, error_message) 1082 | end 1083 | -- ...and if we're still here (because the assert didn't throw an error), 1084 | -- return a nil and throw the error message on as a second arg 1085 | return nil, error_message 1086 | 1087 | end 1088 | end 1089 | 1090 | local function backslash_replacement_function(c) 1091 | if c == "\n" then 1092 | return "\\n" 1093 | elseif c == "\r" then 1094 | return "\\r" 1095 | elseif c == "\t" then 1096 | return "\\t" 1097 | elseif c == "\b" then 1098 | return "\\b" 1099 | elseif c == "\f" then 1100 | return "\\f" 1101 | elseif c == '"' then 1102 | return '\\"' 1103 | elseif c == '\\' then 1104 | return '\\\\' 1105 | else 1106 | return string.format("\\u%04x", c:byte()) 1107 | end 1108 | end 1109 | 1110 | local chars_to_be_escaped_in_JSON_string 1111 | = '[' 1112 | .. '"' -- class sub-pattern to match a double quote 1113 | .. '%\\' -- class sub-pattern to match a backslash 1114 | .. '%z' -- class sub-pattern to match a null 1115 | .. '\001' .. '-' .. '\031' -- class sub-pattern to match control characters 1116 | .. ']' 1117 | 1118 | 1119 | local LINE_SEPARATOR_as_utf8 = unicode_codepoint_as_utf8(0x2028) 1120 | local PARAGRAPH_SEPARATOR_as_utf8 = unicode_codepoint_as_utf8(0x2029) 1121 | local function json_string_literal(value, options) 1122 | local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function) 1123 | if options.stringsAreUtf8 then 1124 | -- 1125 | -- This feels really ugly to just look into a string for the sequence of bytes that we know to be a particular utf8 character, 1126 | -- but utf8 was designed purposefully to make this kind of thing possible. Still, feels dirty. 1127 | -- I'd rather decode the byte stream into a character stream, but it's not technically needed so 1128 | -- not technically worth it. 1129 | -- 1130 | newval = newval:gsub(LINE_SEPARATOR_as_utf8, '\\u2028'):gsub(PARAGRAPH_SEPARATOR_as_utf8,'\\u2029') 1131 | end 1132 | return '"' .. newval .. '"' 1133 | end 1134 | 1135 | local function object_or_array(self, T, etc) 1136 | -- 1137 | -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON 1138 | -- object. If there are only numbers, it's a JSON array. 1139 | -- 1140 | -- If we'll be converting to a JSON object, we'll want to sort the keys so that the 1141 | -- end result is deterministic. 1142 | -- 1143 | local string_keys = { } 1144 | local number_keys = { } 1145 | local number_keys_must_be_strings = false 1146 | local maximum_number_key 1147 | 1148 | for key in pairs(T) do 1149 | if type(key) == 'string' then 1150 | table.insert(string_keys, key) 1151 | elseif type(key) == 'number' then 1152 | table.insert(number_keys, key) 1153 | if key <= 0 or key >= math.huge then 1154 | number_keys_must_be_strings = true 1155 | elseif not maximum_number_key or key > maximum_number_key then 1156 | maximum_number_key = key 1157 | end 1158 | else 1159 | self:onEncodeError("can't encode table with a key of type " .. type(key), etc) 1160 | end 1161 | end 1162 | 1163 | if #string_keys == 0 and not number_keys_must_be_strings then 1164 | -- 1165 | -- An empty table, or a numeric-only array 1166 | -- 1167 | if #number_keys > 0 then 1168 | return nil, maximum_number_key -- an array 1169 | elseif tostring(T) == "JSON array" then 1170 | return nil 1171 | elseif tostring(T) == "JSON object" then 1172 | return { } 1173 | else 1174 | -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects 1175 | return nil 1176 | end 1177 | end 1178 | 1179 | table.sort(string_keys) 1180 | 1181 | local map 1182 | if #number_keys > 0 then 1183 | -- 1184 | -- If we're here then we have either mixed string/number keys, or numbers inappropriate for a JSON array 1185 | -- It's not ideal, but we'll turn the numbers into strings so that we can at least create a JSON object. 1186 | -- 1187 | 1188 | if self.noKeyConversion then 1189 | self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc) 1190 | end 1191 | 1192 | -- 1193 | -- Have to make a shallow copy of the source table so we can remap the numeric keys to be strings 1194 | -- 1195 | map = { } 1196 | for key, val in pairs(T) do 1197 | map[key] = val 1198 | end 1199 | 1200 | table.sort(number_keys) 1201 | 1202 | -- 1203 | -- Throw numeric keys in there as strings 1204 | -- 1205 | for _, number_key in ipairs(number_keys) do 1206 | local string_key = tostring(number_key) 1207 | if map[string_key] == nil then 1208 | table.insert(string_keys , string_key) 1209 | map[string_key] = T[number_key] 1210 | else 1211 | self:onEncodeError("conflict converting table with mixed-type keys into a JSON object: key " .. number_key .. " exists both as a string and a number.", etc) 1212 | end 1213 | end 1214 | end 1215 | 1216 | return string_keys, nil, map 1217 | end 1218 | 1219 | -- 1220 | -- Encode 1221 | -- 1222 | -- 'options' is nil, or a table with possible keys: 1223 | -- 1224 | -- pretty -- If true, return a pretty-printed version. 1225 | -- 1226 | -- indent -- A string (usually of spaces) used to indent each nested level. 1227 | -- 1228 | -- align_keys -- If true, align all the keys when formatting a table. 1229 | -- 1230 | -- null -- If this exists with a string value, table elements with this value are output as JSON null. 1231 | -- 1232 | -- stringsAreUtf8 -- If true, consider Lua strings not as a sequence of bytes, but as a sequence of UTF-8 characters. 1233 | -- (Currently, the only practical effect of setting this option is that Unicode LINE and PARAGRAPH 1234 | -- separators, if found in a string, are encoded with a JSON escape instead of as raw UTF-8. 1235 | -- The JSON is valid either way, but encoding this way, apparently, allows the resulting JSON 1236 | -- to also be valid Java.) 1237 | -- 1238 | -- 1239 | local encode_value -- must predeclare because it calls itself 1240 | function encode_value(self, value, parents, etc, options, indent, for_key) 1241 | 1242 | -- 1243 | -- keys in a JSON object can never be null, so we don't even consider options.null when converting a key value 1244 | -- 1245 | if value == nil or (not for_key and options and options.null and value == options.null) then 1246 | return 'null' 1247 | 1248 | elseif type(value) == 'string' then 1249 | return json_string_literal(value, options) 1250 | 1251 | elseif type(value) == 'number' then 1252 | if value ~= value then 1253 | -- 1254 | -- NaN (Not a Number). 1255 | -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option. 1256 | -- 1257 | return "null" 1258 | elseif value >= math.huge then 1259 | -- 1260 | -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should 1261 | -- really be a package option. Note: at least with some implementations, positive infinity 1262 | -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is. 1263 | -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">=" 1264 | -- case first. 1265 | -- 1266 | return "1e+9999" 1267 | elseif value <= -math.huge then 1268 | -- 1269 | -- Negative infinity. 1270 | -- JSON has no INF, so we have to fudge the best we can. This should really be a package option. 1271 | -- 1272 | return "-1e+9999" 1273 | else 1274 | return tostring(value) 1275 | end 1276 | 1277 | elseif type(value) == 'boolean' then 1278 | return tostring(value) 1279 | 1280 | elseif type(value) ~= 'table' then 1281 | self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc) 1282 | 1283 | elseif getmetatable(value) == isNumber then 1284 | return tostring(value) 1285 | else 1286 | -- 1287 | -- A table to be converted to either a JSON object or array. 1288 | -- 1289 | local T = value 1290 | 1291 | if type(options) ~= 'table' then 1292 | options = {} 1293 | end 1294 | if type(indent) ~= 'string' then 1295 | indent = "" 1296 | end 1297 | 1298 | if parents[T] then 1299 | self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc) 1300 | else 1301 | parents[T] = true 1302 | end 1303 | 1304 | local result_value 1305 | 1306 | local object_keys, maximum_number_key, map = object_or_array(self, T, etc) 1307 | if maximum_number_key then 1308 | -- 1309 | -- An array... 1310 | -- 1311 | local ITEMS = { } 1312 | for i = 1, maximum_number_key do 1313 | table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, indent)) 1314 | end 1315 | 1316 | if options.pretty then 1317 | result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]" 1318 | else 1319 | result_value = "[" .. table.concat(ITEMS, ",") .. "]" 1320 | end 1321 | 1322 | elseif object_keys then 1323 | -- 1324 | -- An object 1325 | -- 1326 | local TT = map or T 1327 | 1328 | if options.pretty then 1329 | 1330 | local KEYS = { } 1331 | local max_key_length = 0 1332 | for _, key in ipairs(object_keys) do 1333 | local encoded = encode_value(self, tostring(key), parents, etc, options, indent, true) 1334 | if options.align_keys then 1335 | max_key_length = math.max(max_key_length, #encoded) 1336 | end 1337 | table.insert(KEYS, encoded) 1338 | end 1339 | local key_indent = indent .. tostring(options.indent or "") 1340 | local subtable_indent = key_indent .. string.rep(" ", max_key_length) .. (options.align_keys and " " or "") 1341 | local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s" 1342 | 1343 | local COMBINED_PARTS = { } 1344 | for i, key in ipairs(object_keys) do 1345 | local encoded_val = encode_value(self, TT[key], parents, etc, options, subtable_indent) 1346 | table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val)) 1347 | end 1348 | result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}" 1349 | 1350 | else 1351 | 1352 | local PARTS = { } 1353 | for _, key in ipairs(object_keys) do 1354 | local encoded_val = encode_value(self, TT[key], parents, etc, options, indent) 1355 | local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent, true) 1356 | table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val)) 1357 | end 1358 | result_value = "{" .. table.concat(PARTS, ",") .. "}" 1359 | 1360 | end 1361 | else 1362 | -- 1363 | -- An empty array/object... we'll treat it as an array, though it should really be an option 1364 | -- 1365 | result_value = "[]" 1366 | end 1367 | 1368 | parents[T] = false 1369 | return result_value 1370 | end 1371 | end 1372 | 1373 | local function top_level_encode(self, value, etc, options) 1374 | local val = encode_value(self, value, {}, etc, options) 1375 | if val == nil then 1376 | --PRIVATE("may need to revert to the previous public verison if I can't figure out what the guy wanted") 1377 | return val 1378 | else 1379 | return val 1380 | end 1381 | end 1382 | 1383 | function OBJDEF:encode(value, etc, options) 1384 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 1385 | OBJDEF:onEncodeError("JSON:encode must be called in method format", etc) 1386 | end 1387 | 1388 | -- 1389 | -- If the user didn't pass in a table of decode options, make an empty one. 1390 | -- 1391 | if type(options) ~= 'table' then 1392 | options = {} 1393 | end 1394 | 1395 | return top_level_encode(self, value, etc, options) 1396 | end 1397 | 1398 | function OBJDEF:encode_pretty(value, etc, options) 1399 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 1400 | OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc) 1401 | end 1402 | 1403 | -- 1404 | -- If the user didn't pass in a table of decode options, use the default pretty ones 1405 | -- 1406 | if type(options) ~= 'table' then 1407 | options = default_pretty_options 1408 | end 1409 | 1410 | return top_level_encode(self, value, etc, options) 1411 | end 1412 | 1413 | function OBJDEF.__tostring() 1414 | return "JSON encode/decode package" 1415 | end 1416 | 1417 | OBJDEF.__index = OBJDEF 1418 | 1419 | function OBJDEF:new(args) 1420 | local new = { } 1421 | 1422 | if args then 1423 | for key, val in pairs(args) do 1424 | new[key] = val 1425 | end 1426 | end 1427 | 1428 | return setmetatable(new, OBJDEF) 1429 | end 1430 | 1431 | return OBJDEF:new() 1432 | 1433 | -- 1434 | -- Version history: 1435 | -- 1436 | -- 20161109.21 Oops, had a small boo-boo in the previous update. 1437 | -- 1438 | -- 20161103.20 Used to silently ignore trailing garbage when decoding. Now fails via JSON:onTrailingGarbage() 1439 | -- http://seriot.ch/parsing_json.php 1440 | -- 1441 | -- Built-in error message about "expected comma or ']'" had mistakenly referred to '[' 1442 | -- 1443 | -- Updated the built-in error reporting to refer to bytes rather than characters. 1444 | -- 1445 | -- The decode() method no longer assumes that error handlers abort. 1446 | -- 1447 | -- Made the VERSION string a string instead of a number 1448 | -- 1449 | 1450 | -- 20160916.19 Fixed the isNumber.__index assignment (thanks to Jack Taylor) 1451 | -- 1452 | -- 20160730.18 Added JSON:forceString() and JSON:forceNumber() 1453 | -- 1454 | -- 20160728.17 Added concatenation to the metatable for JSON:asNumber() 1455 | -- 1456 | -- 20160709.16 Could crash if not passed an options table (thanks jarno heikkinen ). 1457 | -- 1458 | -- Made JSON:asNumber() a bit more resilient to being passed the results of itself. 1459 | -- 1460 | -- 20160526.15 Added the ability to easily encode null values in JSON, via the new "null" encoding option. 1461 | -- (Thanks to Adam B for bringing up the issue.) 1462 | -- 1463 | -- Added some support for very large numbers and precise floats via 1464 | -- JSON.decodeNumbersAsObjects 1465 | -- JSON.decodeIntegerStringificationLength 1466 | -- JSON.decodeDecimalStringificationLength 1467 | -- 1468 | -- Added the "stringsAreUtf8" encoding option. (Hat tip to http://lua-users.org/wiki/JsonModules ) 1469 | -- 1470 | -- 20141223.14 The encode_pretty() routine produced fine results for small datasets, but isn't really 1471 | -- appropriate for anything large, so with help from Alex Aulbach I've made the encode routines 1472 | -- more flexible, and changed the default encode_pretty() to be more generally useful. 1473 | -- 1474 | -- Added a third 'options' argument to the encode() and encode_pretty() routines, to control 1475 | -- how the encoding takes place. 1476 | -- 1477 | -- Updated docs to add assert() call to the loadfile() line, just as good practice so that 1478 | -- if there is a problem loading JSON.lua, the appropriate error message will percolate up. 1479 | -- 1480 | -- 20140920.13 Put back (in a way that doesn't cause warnings about unused variables) the author string, 1481 | -- so that the source of the package, and its version number, are visible in compiled copies. 1482 | -- 1483 | -- 20140911.12 Minor lua cleanup. 1484 | -- Fixed internal reference to 'JSON.noKeyConversion' to reference 'self' instead of 'JSON'. 1485 | -- (Thanks to SmugMug's David Parry for these.) 1486 | -- 1487 | -- 20140418.11 JSON nulls embedded within an array were being ignored, such that 1488 | -- ["1",null,null,null,null,null,"seven"], 1489 | -- would return 1490 | -- {1,"seven"} 1491 | -- It's now fixed to properly return 1492 | -- {1, nil, nil, nil, nil, nil, "seven"} 1493 | -- Thanks to "haddock" for catching the error. 1494 | -- 1495 | -- 20140116.10 The user's JSON.assert() wasn't always being used. Thanks to "blue" for the heads up. 1496 | -- 1497 | -- 20131118.9 Update for Lua 5.3... it seems that tostring(2/1) produces "2.0" instead of "2", 1498 | -- and this caused some problems. 1499 | -- 1500 | -- 20131031.8 Unified the code for encode() and encode_pretty(); they had been stupidly separate, 1501 | -- and had of course diverged (encode_pretty didn't get the fixes that encode got, so 1502 | -- sometimes produced incorrect results; thanks to Mattie for the heads up). 1503 | -- 1504 | -- Handle encoding tables with non-positive numeric keys (unlikely, but possible). 1505 | -- 1506 | -- If a table has both numeric and string keys, or its numeric keys are inappropriate 1507 | -- (such as being non-positive or infinite), the numeric keys are turned into 1508 | -- string keys appropriate for a JSON object. So, as before, 1509 | -- JSON:encode({ "one", "two", "three" }) 1510 | -- produces the array 1511 | -- ["one","two","three"] 1512 | -- but now something with mixed key types like 1513 | -- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) 1514 | -- instead of throwing an error produces an object: 1515 | -- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} 1516 | -- 1517 | -- To maintain the prior throw-an-error semantics, set 1518 | -- JSON.noKeyConversion = true 1519 | -- 1520 | -- 20131004.7 Release under a Creative Commons CC-BY license, which I should have done from day one, sorry. 1521 | -- 1522 | -- 20130120.6 Comment update: added a link to the specific page on my blog where this code can 1523 | -- be found, so that folks who come across the code outside of my blog can find updates 1524 | -- more easily. 1525 | -- 1526 | -- 20111207.5 Added support for the 'etc' arguments, for better error reporting. 1527 | -- 1528 | -- 20110731.4 More feedback from David Kolf on how to make the tests for Nan/Infinity system independent. 1529 | -- 1530 | -- 20110730.3 Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules: 1531 | -- 1532 | -- * When encoding lua for JSON, Sparse numeric arrays are now handled by 1533 | -- spitting out full arrays, such that 1534 | -- JSON:encode({"one", "two", [10] = "ten"}) 1535 | -- returns 1536 | -- ["one","two",null,null,null,null,null,null,null,"ten"] 1537 | -- 1538 | -- In 20100810.2 and earlier, only up to the first non-null value would have been retained. 1539 | -- 1540 | -- * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999". 1541 | -- Version 20100810.2 and earlier created invalid JSON in both cases. 1542 | -- 1543 | -- * Unicode surrogate pairs are now detected when decoding JSON. 1544 | -- 1545 | -- 20100810.2 added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding 1546 | -- 1547 | -- 20100731.1 initial public release 1548 | -- 1549 | --------------------------------------------------------------------------------