├── .gitignore ├── LICENSE.txt ├── README.mdown ├── SITests ├── DJImageHashTest.h ├── DJImageHashTest.m ├── SITests-Info.plist ├── SITests-Prefix.pch ├── SITests.h ├── SITests.m └── en.lproj │ └── InfoPlist.strings ├── SimilarImages.xcodeproj └── project.pbxproj ├── SimilarImages ├── DJImageHash.h ├── DJImageHash.m ├── DJImageTrawler.h ├── DJImageTrawler.m ├── DJPersistentCache.h ├── DJPersistentCache.m ├── DJPersistentCacheTest.h ├── DJPersistentCacheTest.m ├── SIAdditions.h ├── SIAdditions.m ├── SIAppDelegate.h ├── SIAppDelegate.m ├── SIAppIcon.icns ├── SIDefaults.plist ├── SIDocument.h ├── SIDocument.m ├── SIImageBrowserItem.h ├── SIImageBrowserItem.m ├── SIImageView.h ├── SIImageView.m ├── SimilarImages-Info.plist ├── SimilarImages-Prefix.pch ├── SimilarImages.entitlements ├── browse_button_template.pdf ├── en.lproj │ ├── Credits.rtf │ ├── InfoPlist.strings │ ├── MainMenu.xib │ └── SIDocument.xib └── main.m └── Support ├── AppIcon.psd └── browse_button_template.ai /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | SimilarImages 2 | === 3 | 4 | SimilarImages is a Mac application that searches for images on your filesystem using a reference image. It is licensed under the Mozilla Public License, version 2. 5 | 6 | 7 | The algorithm used to determine image similarity is an original implementation of the DCT-based technique described by http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html, which was originally implemented in the open source pHash library. My implementation uses the fftw3 library to perform DCT transforms. -------------------------------------------------------------------------------- /SITests/DJImageHashTest.h: -------------------------------------------------------------------------------- 1 | // 2 | // DJImageHashTest.h 3 | // SimilarImages 4 | // 5 | // Created by Dorian Johnson on 2012/6/25. 6 | // Copyright (c) 2012 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DJImageHashTest : SenTestCase 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /SITests/DJImageHashTest.m: -------------------------------------------------------------------------------- 1 | // 2 | // DJImageHashTest.m 3 | // SimilarImages 4 | // 5 | // Created by Dorian Johnson on 2012/6/25. 6 | // Copyright (c) 2012 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import "DJImageHashTest.h" 10 | 11 | #import "DJImageHash.h" 12 | 13 | @implementation DJImageHashTest 14 | 15 | - (void)testActualImageTransforms 16 | { 17 | DJImageHash* original = [self hashForTestFile:@"group1/bikes.jpg"]; 18 | DJImageHash* original_90deg = [self hashForTestFile:@"group1/bikes 90deg.jpg"]; 19 | DJImageHash* original_180deg = [self hashForTestFile:@"group1/bikes 180deg.jpg"]; 20 | DJImageHash* original_270deg = [self hashForTestFile:@"group1/bikes 270deg.jpg"]; 21 | DJImageHash* original_vert = [self hashForTestFile:@"group1/bikes vert.jpg"]; 22 | DJImageHash* original_horiz = [self hashForTestFile:@"group1/bikes horiz.jpg"]; 23 | NSNumber* similarity; 24 | NSNumber* expected_similarity = [NSNumber numberWithFloat:90]; 25 | 26 | STAssertTrue(original && original_90deg && original_180deg && original_270deg && original_vert && original_horiz, @"Not all test images were found."); 27 | 28 | 29 | STAssertTrue([[original similarityTo:original considerTransforms:NO] compare:expected_similarity] == NSOrderedDescending, @"similarityTo: must 100 for identical inputs."); 30 | 31 | similarity = [original similarityTo:original_90deg considerTransforms:YES]; 32 | STAssertTrue([similarity compare:expected_similarity] == NSOrderedDescending, @"DJImageHashRotate 90deg: similarity should be > %@, but was %@", expected_similarity, similarity); 33 | 34 | similarity = [original similarityTo:original_180deg considerTransforms:YES]; 35 | STAssertTrue([similarity compare:expected_similarity] == NSOrderedDescending, @"DJImageHashRotate 180deg: similarity should be > %@, but was %@", expected_similarity, similarity); 36 | 37 | similarity = [original similarityTo:original_270deg considerTransforms:YES]; 38 | STAssertTrue([similarity compare:expected_similarity] == NSOrderedDescending, @"DJImageHashRotate 270deg: similarity should be > %@, but was %@", expected_similarity, similarity); 39 | 40 | similarity = [original similarityTo:original_vert considerTransforms:YES]; 41 | STAssertTrue([similarity compare:expected_similarity] == NSOrderedDescending, @"DJImageHashVerticalFlip: similarity should be > %@, but was %@", expected_similarity, similarity); 42 | 43 | similarity = [original similarityTo:original_horiz considerTransforms:YES]; 44 | STAssertTrue([similarity compare:expected_similarity] == NSOrderedDescending, @"DJImageHashHorizontalFlip: similarity should be > %@, but was %@", expected_similarity, similarity); 45 | } 46 | 47 | - (DJImageHash*)hashForTestFile:(NSString*)path 48 | { 49 | return [[DJImageHash alloc] initWithURL:[NSURL fileURLWithPath:[[NSString stringWithFormat:@"~/dev/SimilarImages/Support/test_images/%@", path] stringByExpandingTildeInPath]]]; 50 | } 51 | 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /SITests/SITests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | net.dorianj.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SITests/SITests-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'SITests' target in the 'SITests' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /SITests/SITests.h: -------------------------------------------------------------------------------- 1 | /* Copyright 2012 Dorian Johnson <2012@dorianj.net> 2 | */ 3 | 4 | #import 5 | 6 | @interface SITests : SenTestCase 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /SITests/SITests.m: -------------------------------------------------------------------------------- 1 | /* Copyright 2012 Dorian Johnson <2012@dorianj.net> 2 | */ 3 | 4 | #import "SITests.h" 5 | 6 | @implementation SITests 7 | 8 | - (void)setUp 9 | { 10 | [super setUp]; 11 | 12 | // Set-up code here. 13 | } 14 | 15 | - (void)tearDown 16 | { 17 | // Tear-down code here. 18 | 19 | [super tearDown]; 20 | } 21 | 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /SITests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /SimilarImages.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9801805315900EE4007BF938 /* SIAppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 9801805215900EE4007BF938 /* SIAppIcon.icns */; }; 11 | 985D6D53158EA6D1000B56AD /* DJImageHash.m in Sources */ = {isa = PBXBuildFile; fileRef = 985D6D52158EA6D1000B56AD /* DJImageHash.m */; }; 12 | 985D6D57158EA6EE000B56AD /* DJImageTrawler.m in Sources */ = {isa = PBXBuildFile; fileRef = 985D6D56158EA6EE000B56AD /* DJImageTrawler.m */; }; 13 | 985D6D5A158EA80C000B56AD /* SIImageBrowserItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 985D6D59158EA80C000B56AD /* SIImageBrowserItem.m */; }; 14 | 987B6F89158E9E50008463AD /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 987B6F88158E9E50008463AD /* Cocoa.framework */; }; 15 | 987B6F93158E9E50008463AD /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 987B6F91158E9E50008463AD /* InfoPlist.strings */; }; 16 | 987B6F95158E9E50008463AD /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 987B6F94158E9E50008463AD /* main.m */; }; 17 | 987B6F99158E9E50008463AD /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 987B6F97158E9E50008463AD /* Credits.rtf */; }; 18 | 987B6F9C158E9E50008463AD /* SIDocument.m in Sources */ = {isa = PBXBuildFile; fileRef = 987B6F9B158E9E50008463AD /* SIDocument.m */; }; 19 | 987B6F9F158E9E50008463AD /* SIDocument.xib in Resources */ = {isa = PBXBuildFile; fileRef = 987B6F9D158E9E50008463AD /* SIDocument.xib */; }; 20 | 987B6FA2158E9E51008463AD /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 987B6FA0158E9E51008463AD /* MainMenu.xib */; }; 21 | 987B6FAD158EA4AC008463AD /* SIImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 987B6FAC158EA4AC008463AD /* SIImageView.m */; }; 22 | 987B6FAF158EA53D008463AD /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 987B6FAE158EA53D008463AD /* Quartz.framework */; }; 23 | 987B6FB4158EA611008463AD /* browse_button_template.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 987B6FB3158EA611008463AD /* browse_button_template.pdf */; }; 24 | 9881963015939D11003198B8 /* SIDefaults.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9881962F15939D11003198B8 /* SIDefaults.plist */; }; 25 | 9881963915939D79003198B8 /* SIAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9881963815939D79003198B8 /* SIAppDelegate.m */; }; 26 | 9881963B1593C8D0003198B8 /* DJImageHash.m in Sources */ = {isa = PBXBuildFile; fileRef = 985D6D52158EA6D1000B56AD /* DJImageHash.m */; }; 27 | 98819643159928FC003198B8 /* DJImageHashTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 98819642159928FC003198B8 /* DJImageHashTest.m */; }; 28 | 9881964815996774003198B8 /* libfftw3f.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 9881964715996773003198B8 /* libfftw3f.3.dylib */; }; 29 | 9881964915996784003198B8 /* libfftw3f.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 9881964715996773003198B8 /* libfftw3f.3.dylib */; }; 30 | 98FD464115911DC4007D2FF4 /* DJPersistentCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 98FD464015911DC4007D2FF4 /* DJPersistentCache.m */; }; 31 | 98FD464615911E48007D2FF4 /* SIAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 98FD464515911E48007D2FF4 /* SIAdditions.m */; }; 32 | 98FD465A1591517A007D2FF4 /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98FD46591591517A007D2FF4 /* SenTestingKit.framework */; }; 33 | 98FD465B1591517A007D2FF4 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 987B6F88158E9E50008463AD /* Cocoa.framework */; }; 34 | 98FD46611591517A007D2FF4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 98FD465F1591517A007D2FF4 /* InfoPlist.strings */; }; 35 | 98FD46641591517A007D2FF4 /* SITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 98FD46631591517A007D2FF4 /* SITests.m */; }; 36 | 98FD4669159151A9007D2FF4 /* DJPersistentCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 98FD465115914A35007D2FF4 /* DJPersistentCacheTest.m */; }; 37 | 98FD466A1591521E007D2FF4 /* DJPersistentCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 98FD464015911DC4007D2FF4 /* DJPersistentCache.m */; }; 38 | /* End PBXBuildFile section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 9801805215900EE4007BF938 /* SIAppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = SIAppIcon.icns; sourceTree = ""; }; 42 | 985D6D51158EA6D1000B56AD /* DJImageHash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DJImageHash.h; sourceTree = ""; }; 43 | 985D6D52158EA6D1000B56AD /* DJImageHash.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DJImageHash.m; sourceTree = ""; }; 44 | 985D6D55158EA6EE000B56AD /* DJImageTrawler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DJImageTrawler.h; sourceTree = ""; }; 45 | 985D6D56158EA6EE000B56AD /* DJImageTrawler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DJImageTrawler.m; sourceTree = ""; }; 46 | 985D6D58158EA80C000B56AD /* SIImageBrowserItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SIImageBrowserItem.h; sourceTree = ""; }; 47 | 985D6D59158EA80C000B56AD /* SIImageBrowserItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SIImageBrowserItem.m; sourceTree = ""; }; 48 | 985D6D5B158FDE5B000B56AD /* SimilarImages.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = SimilarImages.entitlements; sourceTree = ""; }; 49 | 987B6F84158E9E50008463AD /* SimilarImages.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimilarImages.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | 987B6F88158E9E50008463AD /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 51 | 987B6F8B158E9E50008463AD /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 52 | 987B6F8C158E9E50008463AD /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 53 | 987B6F8D158E9E50008463AD /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 54 | 987B6F90158E9E50008463AD /* SimilarImages-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SimilarImages-Info.plist"; sourceTree = ""; }; 55 | 987B6F92158E9E50008463AD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 56 | 987B6F94158E9E50008463AD /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 57 | 987B6F96158E9E50008463AD /* SimilarImages-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimilarImages-Prefix.pch"; sourceTree = ""; }; 58 | 987B6F98158E9E50008463AD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 59 | 987B6F9A158E9E50008463AD /* SIDocument.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SIDocument.h; sourceTree = ""; }; 60 | 987B6F9B158E9E50008463AD /* SIDocument.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SIDocument.m; sourceTree = ""; }; 61 | 987B6F9E158E9E50008463AD /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/SIDocument.xib; sourceTree = ""; }; 62 | 987B6FA1158E9E51008463AD /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; 63 | 987B6FAB158EA4AC008463AD /* SIImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SIImageView.h; sourceTree = ""; }; 64 | 987B6FAC158EA4AC008463AD /* SIImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SIImageView.m; sourceTree = ""; }; 65 | 987B6FAE158EA53D008463AD /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; }; 66 | 987B6FB3158EA611008463AD /* browse_button_template.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = browse_button_template.pdf; sourceTree = ""; }; 67 | 9881962F15939D11003198B8 /* SIDefaults.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = SIDefaults.plist; sourceTree = ""; }; 68 | 9881963715939D79003198B8 /* SIAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SIAppDelegate.h; sourceTree = ""; }; 69 | 9881963815939D79003198B8 /* SIAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SIAppDelegate.m; sourceTree = ""; }; 70 | 98819641159928FC003198B8 /* DJImageHashTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DJImageHashTest.h; sourceTree = ""; }; 71 | 98819642159928FC003198B8 /* DJImageHashTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DJImageHashTest.m; sourceTree = ""; }; 72 | 9881964715996773003198B8 /* libfftw3f.3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libfftw3f.3.dylib; path = ../../../../usr/local/Cellar/fftw/3.3.2/lib/libfftw3f.3.dylib; sourceTree = ""; }; 73 | 98FD463F15911DC4007D2FF4 /* DJPersistentCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DJPersistentCache.h; sourceTree = ""; }; 74 | 98FD464015911DC4007D2FF4 /* DJPersistentCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DJPersistentCache.m; sourceTree = ""; }; 75 | 98FD464415911E48007D2FF4 /* SIAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SIAdditions.h; sourceTree = ""; }; 76 | 98FD464515911E48007D2FF4 /* SIAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SIAdditions.m; sourceTree = ""; }; 77 | 98FD465015914A35007D2FF4 /* DJPersistentCacheTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DJPersistentCacheTest.h; path = ../SimilarImages/DJPersistentCacheTest.h; sourceTree = ""; }; 78 | 98FD465115914A35007D2FF4 /* DJPersistentCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DJPersistentCacheTest.m; path = ../SimilarImages/DJPersistentCacheTest.m; sourceTree = ""; }; 79 | 98FD465815915179007D2FF4 /* SITests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SITests.octest; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | 98FD46591591517A007D2FF4 /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; 81 | 98FD465E1591517A007D2FF4 /* SITests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SITests-Info.plist"; sourceTree = ""; }; 82 | 98FD46601591517A007D2FF4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 83 | 98FD46621591517A007D2FF4 /* SITests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SITests.h; sourceTree = ""; }; 84 | 98FD46631591517A007D2FF4 /* SITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SITests.m; sourceTree = ""; }; 85 | 98FD46651591517A007D2FF4 /* SITests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SITests-Prefix.pch"; sourceTree = ""; }; 86 | /* End PBXFileReference section */ 87 | 88 | /* Begin PBXFrameworksBuildPhase section */ 89 | 987B6F81158E9E50008463AD /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | 9881964815996774003198B8 /* libfftw3f.3.dylib in Frameworks */, 94 | 987B6FAF158EA53D008463AD /* Quartz.framework in Frameworks */, 95 | 987B6F89158E9E50008463AD /* Cocoa.framework in Frameworks */, 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | 98FD465415915179007D2FF4 /* Frameworks */ = { 100 | isa = PBXFrameworksBuildPhase; 101 | buildActionMask = 2147483647; 102 | files = ( 103 | 98FD465A1591517A007D2FF4 /* SenTestingKit.framework in Frameworks */, 104 | 98FD465B1591517A007D2FF4 /* Cocoa.framework in Frameworks */, 105 | 9881964915996784003198B8 /* libfftw3f.3.dylib in Frameworks */, 106 | ); 107 | runOnlyForDeploymentPostprocessing = 0; 108 | }; 109 | /* End PBXFrameworksBuildPhase section */ 110 | 111 | /* Begin PBXGroup section */ 112 | 987B6F79158E9E50008463AD = { 113 | isa = PBXGroup; 114 | children = ( 115 | 9881964715996773003198B8 /* libfftw3f.3.dylib */, 116 | 987B6F8E158E9E50008463AD /* SimilarImages */, 117 | 98FD465C1591517A007D2FF4 /* SITests */, 118 | 987B6F87158E9E50008463AD /* Frameworks */, 119 | 987B6F85158E9E50008463AD /* Products */, 120 | ); 121 | sourceTree = ""; 122 | }; 123 | 987B6F85158E9E50008463AD /* Products */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 987B6F84158E9E50008463AD /* SimilarImages.app */, 127 | 98FD465815915179007D2FF4 /* SITests.octest */, 128 | ); 129 | name = Products; 130 | sourceTree = ""; 131 | }; 132 | 987B6F87158E9E50008463AD /* Frameworks */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 987B6FAE158EA53D008463AD /* Quartz.framework */, 136 | 987B6F88158E9E50008463AD /* Cocoa.framework */, 137 | 98FD46591591517A007D2FF4 /* SenTestingKit.framework */, 138 | 987B6F8A158E9E50008463AD /* Other Frameworks */, 139 | ); 140 | name = Frameworks; 141 | sourceTree = ""; 142 | }; 143 | 987B6F8A158E9E50008463AD /* Other Frameworks */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 987B6F8B158E9E50008463AD /* AppKit.framework */, 147 | 987B6F8C158E9E50008463AD /* CoreData.framework */, 148 | 987B6F8D158E9E50008463AD /* Foundation.framework */, 149 | ); 150 | name = "Other Frameworks"; 151 | sourceTree = ""; 152 | }; 153 | 987B6F8E158E9E50008463AD /* SimilarImages */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 985D6D5B158FDE5B000B56AD /* SimilarImages.entitlements */, 157 | 987B6FAA158E9E8C008463AD /* Controllers */, 158 | 987B6FA9158E9E87008463AD /* Models */, 159 | 987B6FA8158E9E71008463AD /* Views */, 160 | 98FD464315911E2B007D2FF4 /* Additions */, 161 | 987B6F8F158E9E50008463AD /* Supporting Files */, 162 | ); 163 | path = SimilarImages; 164 | sourceTree = ""; 165 | }; 166 | 987B6F8F158E9E50008463AD /* Supporting Files */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 987B6FB0158EA5EC008463AD /* Resources */, 170 | 987B6F90158E9E50008463AD /* SimilarImages-Info.plist */, 171 | 987B6F91158E9E50008463AD /* InfoPlist.strings */, 172 | 987B6F94158E9E50008463AD /* main.m */, 173 | 987B6F96158E9E50008463AD /* SimilarImages-Prefix.pch */, 174 | 9881962F15939D11003198B8 /* SIDefaults.plist */, 175 | ); 176 | name = "Supporting Files"; 177 | sourceTree = ""; 178 | }; 179 | 987B6FA8158E9E71008463AD /* Views */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | 987B6FA0158E9E51008463AD /* MainMenu.xib */, 183 | 987B6F9D158E9E50008463AD /* SIDocument.xib */, 184 | 987B6FAB158EA4AC008463AD /* SIImageView.h */, 185 | 987B6FAC158EA4AC008463AD /* SIImageView.m */, 186 | ); 187 | name = Views; 188 | sourceTree = ""; 189 | }; 190 | 987B6FA9158E9E87008463AD /* Models */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | 985D6D55158EA6EE000B56AD /* DJImageTrawler.h */, 194 | 985D6D56158EA6EE000B56AD /* DJImageTrawler.m */, 195 | 985D6D51158EA6D1000B56AD /* DJImageHash.h */, 196 | 985D6D52158EA6D1000B56AD /* DJImageHash.m */, 197 | 985D6D58158EA80C000B56AD /* SIImageBrowserItem.h */, 198 | 985D6D59158EA80C000B56AD /* SIImageBrowserItem.m */, 199 | 98FD463F15911DC4007D2FF4 /* DJPersistentCache.h */, 200 | 98FD464015911DC4007D2FF4 /* DJPersistentCache.m */, 201 | ); 202 | name = Models; 203 | sourceTree = ""; 204 | }; 205 | 987B6FAA158E9E8C008463AD /* Controllers */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | 987B6F9A158E9E50008463AD /* SIDocument.h */, 209 | 987B6F9B158E9E50008463AD /* SIDocument.m */, 210 | 9881963715939D79003198B8 /* SIAppDelegate.h */, 211 | 9881963815939D79003198B8 /* SIAppDelegate.m */, 212 | ); 213 | name = Controllers; 214 | sourceTree = ""; 215 | }; 216 | 987B6FB0158EA5EC008463AD /* Resources */ = { 217 | isa = PBXGroup; 218 | children = ( 219 | 9801805215900EE4007BF938 /* SIAppIcon.icns */, 220 | 987B6FB3158EA611008463AD /* browse_button_template.pdf */, 221 | 987B6F97158E9E50008463AD /* Credits.rtf */, 222 | ); 223 | name = Resources; 224 | sourceTree = ""; 225 | }; 226 | 98FD464315911E2B007D2FF4 /* Additions */ = { 227 | isa = PBXGroup; 228 | children = ( 229 | 98FD464415911E48007D2FF4 /* SIAdditions.h */, 230 | 98FD464515911E48007D2FF4 /* SIAdditions.m */, 231 | ); 232 | name = Additions; 233 | sourceTree = ""; 234 | }; 235 | 98FD465C1591517A007D2FF4 /* SITests */ = { 236 | isa = PBXGroup; 237 | children = ( 238 | 98FD46621591517A007D2FF4 /* SITests.h */, 239 | 98FD46631591517A007D2FF4 /* SITests.m */, 240 | 98819641159928FC003198B8 /* DJImageHashTest.h */, 241 | 98819642159928FC003198B8 /* DJImageHashTest.m */, 242 | 98FD465015914A35007D2FF4 /* DJPersistentCacheTest.h */, 243 | 98FD465115914A35007D2FF4 /* DJPersistentCacheTest.m */, 244 | 98FD465D1591517A007D2FF4 /* Supporting Files */, 245 | ); 246 | path = SITests; 247 | sourceTree = ""; 248 | }; 249 | 98FD465D1591517A007D2FF4 /* Supporting Files */ = { 250 | isa = PBXGroup; 251 | children = ( 252 | 98FD465E1591517A007D2FF4 /* SITests-Info.plist */, 253 | 98FD465F1591517A007D2FF4 /* InfoPlist.strings */, 254 | 98FD46651591517A007D2FF4 /* SITests-Prefix.pch */, 255 | ); 256 | name = "Supporting Files"; 257 | sourceTree = ""; 258 | }; 259 | /* End PBXGroup section */ 260 | 261 | /* Begin PBXNativeTarget section */ 262 | 987B6F83158E9E50008463AD /* SimilarImages */ = { 263 | isa = PBXNativeTarget; 264 | buildConfigurationList = 987B6FA5158E9E51008463AD /* Build configuration list for PBXNativeTarget "SimilarImages" */; 265 | buildPhases = ( 266 | 987B6F80158E9E50008463AD /* Sources */, 267 | 987B6F81158E9E50008463AD /* Frameworks */, 268 | 987B6F82158E9E50008463AD /* Resources */, 269 | ); 270 | buildRules = ( 271 | ); 272 | dependencies = ( 273 | ); 274 | name = SimilarImages; 275 | productName = SimilarImages; 276 | productReference = 987B6F84158E9E50008463AD /* SimilarImages.app */; 277 | productType = "com.apple.product-type.application"; 278 | }; 279 | 98FD465715915179007D2FF4 /* SITests */ = { 280 | isa = PBXNativeTarget; 281 | buildConfigurationList = 98FD46661591517A007D2FF4 /* Build configuration list for PBXNativeTarget "SITests" */; 282 | buildPhases = ( 283 | 98FD465315915179007D2FF4 /* Sources */, 284 | 98FD465415915179007D2FF4 /* Frameworks */, 285 | 98FD465515915179007D2FF4 /* Resources */, 286 | 98FD465615915179007D2FF4 /* ShellScript */, 287 | ); 288 | buildRules = ( 289 | ); 290 | dependencies = ( 291 | ); 292 | name = SITests; 293 | productName = SITests; 294 | productReference = 98FD465815915179007D2FF4 /* SITests.octest */; 295 | productType = "com.apple.product-type.bundle"; 296 | }; 297 | /* End PBXNativeTarget section */ 298 | 299 | /* Begin PBXProject section */ 300 | 987B6F7B158E9E50008463AD /* Project object */ = { 301 | isa = PBXProject; 302 | attributes = { 303 | CLASSPREFIX = SI; 304 | LastUpgradeCheck = 0430; 305 | }; 306 | buildConfigurationList = 987B6F7E158E9E50008463AD /* Build configuration list for PBXProject "SimilarImages" */; 307 | compatibilityVersion = "Xcode 3.2"; 308 | developmentRegion = English; 309 | hasScannedForEncodings = 0; 310 | knownRegions = ( 311 | en, 312 | ); 313 | mainGroup = 987B6F79158E9E50008463AD; 314 | productRefGroup = 987B6F85158E9E50008463AD /* Products */; 315 | projectDirPath = ""; 316 | projectRoot = ""; 317 | targets = ( 318 | 987B6F83158E9E50008463AD /* SimilarImages */, 319 | 98FD465715915179007D2FF4 /* SITests */, 320 | ); 321 | }; 322 | /* End PBXProject section */ 323 | 324 | /* Begin PBXResourcesBuildPhase section */ 325 | 987B6F82158E9E50008463AD /* Resources */ = { 326 | isa = PBXResourcesBuildPhase; 327 | buildActionMask = 2147483647; 328 | files = ( 329 | 987B6F93158E9E50008463AD /* InfoPlist.strings in Resources */, 330 | 987B6F99158E9E50008463AD /* Credits.rtf in Resources */, 331 | 987B6F9F158E9E50008463AD /* SIDocument.xib in Resources */, 332 | 987B6FA2158E9E51008463AD /* MainMenu.xib in Resources */, 333 | 987B6FB4158EA611008463AD /* browse_button_template.pdf in Resources */, 334 | 9801805315900EE4007BF938 /* SIAppIcon.icns in Resources */, 335 | 9881963015939D11003198B8 /* SIDefaults.plist in Resources */, 336 | ); 337 | runOnlyForDeploymentPostprocessing = 0; 338 | }; 339 | 98FD465515915179007D2FF4 /* Resources */ = { 340 | isa = PBXResourcesBuildPhase; 341 | buildActionMask = 2147483647; 342 | files = ( 343 | 98FD46611591517A007D2FF4 /* InfoPlist.strings in Resources */, 344 | ); 345 | runOnlyForDeploymentPostprocessing = 0; 346 | }; 347 | /* End PBXResourcesBuildPhase section */ 348 | 349 | /* Begin PBXShellScriptBuildPhase section */ 350 | 98FD465615915179007D2FF4 /* ShellScript */ = { 351 | isa = PBXShellScriptBuildPhase; 352 | buildActionMask = 2147483647; 353 | files = ( 354 | ); 355 | inputPaths = ( 356 | ); 357 | outputPaths = ( 358 | ); 359 | runOnlyForDeploymentPostprocessing = 0; 360 | shellPath = /bin/sh; 361 | shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n"; 362 | }; 363 | /* End PBXShellScriptBuildPhase section */ 364 | 365 | /* Begin PBXSourcesBuildPhase section */ 366 | 987B6F80158E9E50008463AD /* Sources */ = { 367 | isa = PBXSourcesBuildPhase; 368 | buildActionMask = 2147483647; 369 | files = ( 370 | 987B6F95158E9E50008463AD /* main.m in Sources */, 371 | 987B6F9C158E9E50008463AD /* SIDocument.m in Sources */, 372 | 987B6FAD158EA4AC008463AD /* SIImageView.m in Sources */, 373 | 985D6D53158EA6D1000B56AD /* DJImageHash.m in Sources */, 374 | 985D6D57158EA6EE000B56AD /* DJImageTrawler.m in Sources */, 375 | 985D6D5A158EA80C000B56AD /* SIImageBrowserItem.m in Sources */, 376 | 98FD464115911DC4007D2FF4 /* DJPersistentCache.m in Sources */, 377 | 98FD464615911E48007D2FF4 /* SIAdditions.m in Sources */, 378 | 9881963915939D79003198B8 /* SIAppDelegate.m in Sources */, 379 | ); 380 | runOnlyForDeploymentPostprocessing = 0; 381 | }; 382 | 98FD465315915179007D2FF4 /* Sources */ = { 383 | isa = PBXSourcesBuildPhase; 384 | buildActionMask = 2147483647; 385 | files = ( 386 | 98FD46641591517A007D2FF4 /* SITests.m in Sources */, 387 | 98FD4669159151A9007D2FF4 /* DJPersistentCacheTest.m in Sources */, 388 | 98FD466A1591521E007D2FF4 /* DJPersistentCache.m in Sources */, 389 | 9881963B1593C8D0003198B8 /* DJImageHash.m in Sources */, 390 | 98819643159928FC003198B8 /* DJImageHashTest.m in Sources */, 391 | ); 392 | runOnlyForDeploymentPostprocessing = 0; 393 | }; 394 | /* End PBXSourcesBuildPhase section */ 395 | 396 | /* Begin PBXVariantGroup section */ 397 | 987B6F91158E9E50008463AD /* InfoPlist.strings */ = { 398 | isa = PBXVariantGroup; 399 | children = ( 400 | 987B6F92158E9E50008463AD /* en */, 401 | ); 402 | name = InfoPlist.strings; 403 | sourceTree = ""; 404 | }; 405 | 987B6F97158E9E50008463AD /* Credits.rtf */ = { 406 | isa = PBXVariantGroup; 407 | children = ( 408 | 987B6F98158E9E50008463AD /* en */, 409 | ); 410 | name = Credits.rtf; 411 | sourceTree = ""; 412 | }; 413 | 987B6F9D158E9E50008463AD /* SIDocument.xib */ = { 414 | isa = PBXVariantGroup; 415 | children = ( 416 | 987B6F9E158E9E50008463AD /* en */, 417 | ); 418 | name = SIDocument.xib; 419 | sourceTree = ""; 420 | }; 421 | 987B6FA0158E9E51008463AD /* MainMenu.xib */ = { 422 | isa = PBXVariantGroup; 423 | children = ( 424 | 987B6FA1158E9E51008463AD /* en */, 425 | ); 426 | name = MainMenu.xib; 427 | sourceTree = ""; 428 | }; 429 | 98FD465F1591517A007D2FF4 /* InfoPlist.strings */ = { 430 | isa = PBXVariantGroup; 431 | children = ( 432 | 98FD46601591517A007D2FF4 /* en */, 433 | ); 434 | name = InfoPlist.strings; 435 | sourceTree = ""; 436 | }; 437 | /* End PBXVariantGroup section */ 438 | 439 | /* Begin XCBuildConfiguration section */ 440 | 987B6FA3158E9E51008463AD /* Debug */ = { 441 | isa = XCBuildConfiguration; 442 | buildSettings = { 443 | ALWAYS_SEARCH_USER_PATHS = NO; 444 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 445 | CLANG_ENABLE_OBJC_ARC = YES; 446 | CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; 447 | COPY_PHASE_STRIP = NO; 448 | GCC_C_LANGUAGE_STANDARD = gnu99; 449 | GCC_DYNAMIC_NO_PIC = NO; 450 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 451 | GCC_OPTIMIZATION_LEVEL = 0; 452 | GCC_PREPROCESSOR_DEFINITIONS = ( 453 | "DEBUG=1", 454 | "$(inherited)", 455 | ); 456 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 457 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 458 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 459 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 460 | GCC_WARN_SIGN_COMPARE = YES; 461 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 462 | GCC_WARN_UNUSED_VARIABLE = NO; 463 | MACOSX_DEPLOYMENT_TARGET = 10.7; 464 | ONLY_ACTIVE_ARCH = YES; 465 | SDKROOT = macosx; 466 | }; 467 | name = Debug; 468 | }; 469 | 987B6FA4158E9E51008463AD /* Release */ = { 470 | isa = XCBuildConfiguration; 471 | buildSettings = { 472 | ALWAYS_SEARCH_USER_PATHS = NO; 473 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 474 | CLANG_ENABLE_OBJC_ARC = YES; 475 | CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; 476 | COPY_PHASE_STRIP = YES; 477 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 478 | GCC_C_LANGUAGE_STANDARD = gnu99; 479 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 480 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 481 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 482 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 483 | GCC_WARN_SIGN_COMPARE = YES; 484 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 485 | GCC_WARN_UNUSED_VARIABLE = YES; 486 | MACOSX_DEPLOYMENT_TARGET = 10.7; 487 | SDKROOT = macosx; 488 | }; 489 | name = Release; 490 | }; 491 | 987B6FA6158E9E51008463AD /* Debug */ = { 492 | isa = XCBuildConfiguration; 493 | buildSettings = { 494 | CODE_SIGN_ENTITLEMENTS = SimilarImages/SimilarImages.entitlements; 495 | CODE_SIGN_IDENTITY = "Developer ID Application: Dorian Johnson"; 496 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 497 | GCC_PREFIX_HEADER = "SimilarImages/SimilarImages-Prefix.pch"; 498 | INFOPLIST_FILE = "SimilarImages/SimilarImages-Info.plist"; 499 | LIBRARY_SEARCH_PATHS = ( 500 | "$(inherited)", 501 | /usr/local/Cellar/fftw/3.3.2/lib, 502 | ); 503 | PRODUCT_NAME = "$(TARGET_NAME)"; 504 | PROVISIONING_PROFILE = ""; 505 | WRAPPER_EXTENSION = app; 506 | }; 507 | name = Debug; 508 | }; 509 | 987B6FA7158E9E51008463AD /* Release */ = { 510 | isa = XCBuildConfiguration; 511 | buildSettings = { 512 | CODE_SIGN_ENTITLEMENTS = SimilarImages/SimilarImages.entitlements; 513 | CODE_SIGN_IDENTITY = "Developer ID Application: Dorian Johnson"; 514 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 515 | GCC_PREFIX_HEADER = "SimilarImages/SimilarImages-Prefix.pch"; 516 | INFOPLIST_FILE = "SimilarImages/SimilarImages-Info.plist"; 517 | LIBRARY_SEARCH_PATHS = ( 518 | "$(inherited)", 519 | /usr/local/Cellar/fftw/3.3.2/lib, 520 | ); 521 | PRODUCT_NAME = "$(TARGET_NAME)"; 522 | PROVISIONING_PROFILE = ""; 523 | WRAPPER_EXTENSION = app; 524 | }; 525 | name = Release; 526 | }; 527 | 98FD46671591517A007D2FF4 /* Debug */ = { 528 | isa = XCBuildConfiguration; 529 | buildSettings = { 530 | FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; 531 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 532 | GCC_PREFIX_HEADER = "SITests/SITests-Prefix.pch"; 533 | INFOPLIST_FILE = "SITests/SITests-Info.plist"; 534 | LIBRARY_SEARCH_PATHS = ( 535 | "$(inherited)", 536 | /usr/local/Cellar/fftw/3.3.2/lib, 537 | ); 538 | PRODUCT_NAME = "$(TARGET_NAME)"; 539 | WRAPPER_EXTENSION = octest; 540 | }; 541 | name = Debug; 542 | }; 543 | 98FD46681591517A007D2FF4 /* Release */ = { 544 | isa = XCBuildConfiguration; 545 | buildSettings = { 546 | FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; 547 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 548 | GCC_PREFIX_HEADER = "SITests/SITests-Prefix.pch"; 549 | INFOPLIST_FILE = "SITests/SITests-Info.plist"; 550 | LIBRARY_SEARCH_PATHS = ( 551 | "$(inherited)", 552 | /usr/local/Cellar/fftw/3.3.2/lib, 553 | ); 554 | PRODUCT_NAME = "$(TARGET_NAME)"; 555 | WRAPPER_EXTENSION = octest; 556 | }; 557 | name = Release; 558 | }; 559 | /* End XCBuildConfiguration section */ 560 | 561 | /* Begin XCConfigurationList section */ 562 | 987B6F7E158E9E50008463AD /* Build configuration list for PBXProject "SimilarImages" */ = { 563 | isa = XCConfigurationList; 564 | buildConfigurations = ( 565 | 987B6FA3158E9E51008463AD /* Debug */, 566 | 987B6FA4158E9E51008463AD /* Release */, 567 | ); 568 | defaultConfigurationIsVisible = 0; 569 | defaultConfigurationName = Release; 570 | }; 571 | 987B6FA5158E9E51008463AD /* Build configuration list for PBXNativeTarget "SimilarImages" */ = { 572 | isa = XCConfigurationList; 573 | buildConfigurations = ( 574 | 987B6FA6158E9E51008463AD /* Debug */, 575 | 987B6FA7158E9E51008463AD /* Release */, 576 | ); 577 | defaultConfigurationIsVisible = 0; 578 | defaultConfigurationName = Release; 579 | }; 580 | 98FD46661591517A007D2FF4 /* Build configuration list for PBXNativeTarget "SITests" */ = { 581 | isa = XCConfigurationList; 582 | buildConfigurations = ( 583 | 98FD46671591517A007D2FF4 /* Debug */, 584 | 98FD46681591517A007D2FF4 /* Release */, 585 | ); 586 | defaultConfigurationIsVisible = 0; 587 | defaultConfigurationName = Release; 588 | }; 589 | /* End XCConfigurationList section */ 590 | }; 591 | rootObject = 987B6F7B158E9E50008463AD /* Project object */; 592 | } 593 | -------------------------------------------------------------------------------- /SimilarImages/DJImageHash.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | 7 | #import 8 | 9 | @interface DJImageHash : NSObject 10 | 11 | @property (readwrite, retain) NSURL* URL; 12 | @property (readwrite, retain) NSImage* image; 13 | @property (readonly) NSInteger version; 14 | 15 | - (id)initWithURL:(NSURL*)url; 16 | 17 | // The current version number of the class. 18 | + (NSInteger)latestVersion; 19 | 20 | 21 | // Force the receiver to open image and calculate the hash; optionally, also calculate the hashes. 22 | - (BOOL)calculateHashWithTransforms:(BOOL)calculateTransformedHashes; 23 | 24 | // Returns a number between 0-100 representing how close the match is. 25 | - (NSNumber*)similarityTo:(DJImageHash*)hash considerTransforms:(BOOL)transform; 26 | @end 27 | 28 | 29 | 30 | 31 | /* 32 | 33 | typedef uint64_t image_hash_t; 34 | 35 | // Misc 36 | NSInteger DJImageHashVersion(); 37 | 38 | #pragma mark Calculating hashes 39 | 40 | image_hash_t DJImageHashFromURL(NSURL* image); 41 | image_hash_t DJImageHashFromURLWithTransforms(NSURL* image); 42 | 43 | 44 | #pragma mark Transforming hashes 45 | 46 | // Rotate a hash. `degrees' must equal one of: 0, 90, 180, 270 47 | image_hash_t DJImageHashRotate(image_hash_t hash, NSInteger degrees); 48 | 49 | // Flip a hash. 50 | image_hash_t DJImageHashVerticalFlip(image_hash_t hash); 51 | image_hash_t DJImageHashHorizontalFlip(NSUInteger hash); 52 | 53 | 54 | #pragma mark Comparing Hashes 55 | 56 | // Bit distance between two hashes 57 | NSInteger DJImageHashCompare(image_hash_t hash1, image_hash_t hash2); 58 | 59 | // Bit distance between two hashes; first image will be transformed using all available transforms; closest distance will be returned. 60 | NSInteger DJImageHashCompareWithTransforms(image_hash_t hash1, image_hash_t hash2); 61 | */ -------------------------------------------------------------------------------- /SimilarImages/DJImageHash.m: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | 7 | /* 8 | Implements the "averaging" technique found here: http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html 9 | */ 10 | 11 | #import "DjImageHash.h" 12 | 13 | #define HAMMING_DISTANCE(A,B) ({ unsigned long long __BD = (A) ^ (B); __builtin_popcountll(__BD); }) 14 | 15 | typedef enum _DJImageHashTypes { 16 | DJImageHashTypeDCT = 0, 17 | DJImageHashTypeDCTRotated90Degrees, 18 | DJImageHashTypeDCTRotated180Degrees, 19 | DJImageHashTypeDCTRotated270Degrees, 20 | DJImageHashTypeDCTFlippedHorizontally, 21 | DJImageHashTypeDCTFlippedVertically 22 | } DJImageHashTypes; 23 | 24 | 25 | #include 26 | 27 | #include 28 | 29 | @interface DJImageHash () 30 | @property (readwrite) NSInteger version; 31 | @property (readwrite) NSDictionary* hashes; 32 | @end 33 | 34 | static const int DCT_DOWNSAMPLE_SIZE = 32; 35 | 36 | OSSpinLock imagehash_fftw_lock = OS_SPINLOCK_INIT; 37 | 38 | 39 | @implementation DJImageHash 40 | 41 | @synthesize URL, version, hashes, image; 42 | 43 | - (id)init 44 | { 45 | if (!(self = [super init])) 46 | return nil; 47 | 48 | return self; 49 | } 50 | 51 | - (id)initWithURL:(NSURL*)url 52 | { 53 | if (!(self = [self init])) 54 | return nil; 55 | 56 | [self setURL:url]; 57 | 58 | return self; 59 | } 60 | 61 | + (NSInteger)latestVersion 62 | { 63 | return 14; 64 | } 65 | 66 | 67 | #pragma mark - 68 | #pragma mark NSCoding 69 | 70 | - (id)initWithCoder:(NSCoder *)decoder 71 | { 72 | if (!(self = [self init])) 73 | return nil; 74 | 75 | if ([decoder containsValueForKey:@"v"]) 76 | [self setVersion:[decoder decodeIntegerForKey:@"v"]]; 77 | 78 | if ([decoder containsValueForKey:@"h"]) 79 | [self setHashes:[decoder decodeObjectForKey:@"h"]]; 80 | 81 | return self; 82 | } 83 | 84 | - (void)encodeWithCoder:(NSCoder *)coder 85 | { 86 | [coder encodeInteger:[self version] forKey:@"v"]; 87 | 88 | if ([self hashes]) 89 | [coder encodeObject:[self hashes] forKey:@"h"]; 90 | } 91 | 92 | 93 | #pragma mark - 94 | #pragma mark Comparing hashes 95 | 96 | - (NSNumber*)similarityTo:(DJImageHash*)hash considerTransforms:(BOOL)transform 97 | { 98 | if ( ![self haveCalculatedHashWithTransforms:transform] && ![self calculateHashWithTransforms:transform]) 99 | return nil; 100 | 101 | if ( ![hash haveCalculatedHash] && ![hash calculateHashWithTransforms:NO]) 102 | return nil; 103 | 104 | NSInteger smallestDistance = 64; 105 | NSUInteger comparisonHashValue = [[[hash hashes] objectForKey:[NSNumber numberWithInteger:DJImageHashTypeDCT]] unsignedIntegerValue]; 106 | 107 | if (transform) 108 | { 109 | for (NSNumber* hashValue in [[self hashes] allValues]) 110 | { 111 | NSInteger distance = HAMMING_DISTANCE([hashValue unsignedIntegerValue], comparisonHashValue); 112 | 113 | if (distance < smallestDistance) 114 | smallestDistance = distance; 115 | } 116 | } 117 | else 118 | { 119 | smallestDistance = HAMMING_DISTANCE([[[self hashes] objectForKey:[NSNumber numberWithInteger:DJImageHashTypeDCT]] unsignedIntegerValue], comparisonHashValue); 120 | } 121 | 122 | 123 | // Convert hamming distance into 0 - 100 score. Distance of 0-18 is a certain match. 19-22 is a potential match. 23-26 is an unlikely match. Anything more is extremely unlikely. This unscientific mapping produces a rough probability distribution of a match. 124 | double x = 100 - pow(smallestDistance, 2.5) / 35.0; 125 | 126 | if (x < 0.0) 127 | return [NSNumber numberWithBool:NO]; 128 | 129 | return [NSNumber numberWithDouble:x]; 130 | } 131 | 132 | 133 | #pragma mark - 134 | #pragma mark Transforms 135 | 136 | - (BOOL)canTransform 137 | { 138 | return [self URL] != nil; 139 | } 140 | 141 | - (BOOL)haveCalculatedHashWithTransforms:(BOOL)transforms 142 | { 143 | return (transforms) ? [self haveCalculatedTransforms] : [self haveCalculatedHash]; 144 | } 145 | - (BOOL)haveCalculatedHash 146 | { 147 | return [[self hashes] count] >= 1; 148 | } 149 | 150 | - (BOOL)haveCalculatedTransforms 151 | { 152 | return [[self hashes] count] > 1; 153 | } 154 | 155 | #pragma mark - 156 | #pragma mark Serializing FFTW accesses 157 | 158 | + (void)lockFFTW 159 | { 160 | OSSpinLockLock(&imagehash_fftw_lock); 161 | 162 | } 163 | 164 | + (void)unlockFFTW 165 | { 166 | OSSpinLockUnlock(&imagehash_fftw_lock); 167 | 168 | } 169 | 170 | 171 | #pragma mark - 172 | #pragma mark Calculating hashes 173 | 174 | - (BOOL)calculateHashWithTransforms:(BOOL)calculateTransformedHashes 175 | { 176 | return [self _calculateDCTHashesWithTransforms:calculateTransformedHashes]; 177 | } 178 | 179 | - (BOOL)_calculateDCTHashesWithTransforms:(BOOL)transforms 180 | { 181 | static CGColorSpaceRef _gray_color_space; 182 | static CFDictionaryRef _image_source_options; 183 | static BOOL _initialized = NO; 184 | 185 | if ([self image]) 186 | { 187 | CGImageRef imageRef = [[self image] CGImageForProposedRect:NULL context:nil hints:nil]; 188 | 189 | if (imageRef == NULL) 190 | return NO; 191 | 192 | [self setHashes:[self _calculateDCTHashesForCGImage:imageRef transforms:transforms]]; 193 | return [[self hashes] count] > 0; 194 | } 195 | 196 | 197 | if (!_initialized) 198 | { 199 | _gray_color_space = CGColorSpaceCreateDeviceGray(); 200 | _image_source_options = (__bridge_retained CFDictionaryRef)[[NSDictionary alloc] initWithObjectsAndKeys: 201 | [NSNumber numberWithUnsignedInteger:DCT_DOWNSAMPLE_SIZE*2], kCGImageSourceThumbnailMaxPixelSize, /* double pixel resolution becuase thumbnail creator respects aspect ratio */ 202 | [NSNumber numberWithBool:NO], kCGImageSourceShouldCache, 203 | nil]; 204 | _initialized = YES; 205 | } 206 | 207 | 208 | CGImageSourceRef image_source = CGImageSourceCreateWithURL((__bridge CFURLRef)[self URL], _image_source_options); 209 | 210 | if (image_source == NULL) 211 | { 212 | NSLog(@"%s: couldn't load image source %@; file probably doesn't exist.", __func__, [self URL]); 213 | return NO; 214 | } 215 | 216 | CGImageRef thumbnail_image = CGImageSourceCreateThumbnailAtIndex(image_source, 0, _image_source_options); 217 | 218 | if (thumbnail_image == NULL) 219 | thumbnail_image = CGImageSourceCreateImageAtIndex(image_source, 0, _image_source_options); 220 | 221 | CFRelease(image_source); 222 | 223 | if (thumbnail_image == NULL) 224 | { 225 | NSLog(@"%s: couldn't generate thumbnail. Image data is probably corrupt.", __func__); 226 | return NO; 227 | } 228 | 229 | [self setHashes:[self _calculateDCTHashesForCGImage:thumbnail_image transforms:transforms]]; 230 | CGImageRelease(thumbnail_image); 231 | 232 | return [[self hashes] count] > 0; 233 | } 234 | 235 | - (NSDictionary*)_calculateDCTHashesForCGImage:(CGImageRef)imageRef transforms:(BOOL)calculateTransforms 236 | { 237 | static CGColorSpaceRef _gray_color_space = NULL; 238 | 239 | if (_gray_color_space == NULL) 240 | _gray_color_space = CGColorSpaceCreateDeviceGray(); 241 | 242 | NSMutableDictionary* calculatedHashes = [NSMutableDictionary dictionary]; 243 | uint8 data[DCT_DOWNSAMPLE_SIZE * DCT_DOWNSAMPLE_SIZE]; 244 | 245 | // Create the gray representation, will be left in `data' 246 | CGContextRef bcontext = CGBitmapContextCreate(data, DCT_DOWNSAMPLE_SIZE, DCT_DOWNSAMPLE_SIZE, 8, DCT_DOWNSAMPLE_SIZE, _gray_color_space, kCGImageAlphaNone); 247 | CGRect drawImageRect = CGRectMake(0, 0, DCT_DOWNSAMPLE_SIZE, DCT_DOWNSAMPLE_SIZE); 248 | CGContextSetInterpolationQuality(bcontext, kCGInterpolationHigh); 249 | CGContextDrawImage(bcontext, drawImageRect, imageRef); 250 | 251 | // Hash the original image. 252 | [calculatedHashes setObject:[self _dctHashForImageData:data] forKey:[NSNumber numberWithInteger:DJImageHashTypeDCT]]; 253 | 254 | // If requested, create transformed hashes as well. 255 | if (calculateTransforms) 256 | { 257 | [self writeBitmapContext:bcontext toDebugFile:@"orig"]; 258 | 259 | // The three rotations: 90, 180, 270. One extra element to get the CTM back to a normal state. 260 | for (id transformIndex in [NSArray arrayWithObjects:[NSNumber numberWithInteger:DJImageHashTypeDCTRotated90Degrees], [NSNumber numberWithInteger:DJImageHashTypeDCTRotated180Degrees], [NSNumber numberWithInteger:DJImageHashTypeDCTRotated270Degrees], [NSNull null], nil]) 261 | { 262 | CGContextTranslateCTM(bcontext, DCT_DOWNSAMPLE_SIZE/2, DCT_DOWNSAMPLE_SIZE/2); 263 | CGContextRotateCTM(bcontext, M_PI / 2.0); 264 | CGContextTranslateCTM(bcontext, -DCT_DOWNSAMPLE_SIZE/2, -DCT_DOWNSAMPLE_SIZE/2); 265 | 266 | if ([transformIndex isKindOfClass:[NSNull class]]) 267 | break; 268 | 269 | CGContextDrawImage(bcontext, drawImageRect, imageRef); 270 | [self writeBitmapContext:bcontext toDebugFile:[NSString stringWithFormat:@"rot-%@", transformIndex]]; 271 | [calculatedHashes setObject:[self _dctHashForImageData:data] forKey:transformIndex]; 272 | } 273 | 274 | // Flip horizontally. 275 | CGContextScaleCTM(bcontext, -1.0, 1.0); 276 | CGContextTranslateCTM(bcontext, -DCT_DOWNSAMPLE_SIZE, 0); 277 | CGContextDrawImage(bcontext, drawImageRect, imageRef); 278 | [self writeBitmapContext:bcontext toDebugFile:@"horiz"]; 279 | [calculatedHashes setObject:[self _dctHashForImageData:data] forKey:[NSNumber numberWithInteger:DJImageHashTypeDCTFlippedHorizontally]]; 280 | 281 | 282 | // Flip vertically. 283 | CGContextScaleCTM(bcontext, -1.0, -1.0); 284 | CGContextTranslateCTM(bcontext, -DCT_DOWNSAMPLE_SIZE, -DCT_DOWNSAMPLE_SIZE); 285 | CGContextDrawImage(bcontext, drawImageRect, imageRef); 286 | [self writeBitmapContext:bcontext toDebugFile:@"vert"]; 287 | [calculatedHashes setObject:[self _dctHashForImageData:data] forKey:[NSNumber numberWithInteger:DJImageHashTypeDCTFlippedVertically]]; 288 | } 289 | 290 | CGContextRelease(bcontext); 291 | return calculatedHashes; 292 | } 293 | 294 | 295 | - (NSNumber*)_dctHashForImageData:(unsigned char*)data 296 | { 297 | float dct_data[DCT_DOWNSAMPLE_SIZE*DCT_DOWNSAMPLE_SIZE]; 298 | 299 | for (int i = 0; i < (int)(DCT_DOWNSAMPLE_SIZE*DCT_DOWNSAMPLE_SIZE); i++) 300 | dct_data[i] = (double)data[i] - 127; 301 | 302 | fftwf_plan plan; 303 | 304 | // Do the DCT. 305 | [[self class] lockFFTW]; 306 | plan = fftwf_plan_r2r_2d(DCT_DOWNSAMPLE_SIZE, DCT_DOWNSAMPLE_SIZE, dct_data, dct_data, FFTW_REDFT10, FFTW_REDFT10, FFTW_ESTIMATE); 307 | [[self class] unlockFFTW]; 308 | 309 | fftwf_execute(plan); 310 | 311 | [[self class] lockFFTW]; 312 | fftwf_destroy_plan(plan); 313 | [[self class] unlockFFTW]; 314 | 315 | 316 | /* Debugging: write out an inverse-fft copy. 317 | for (int i = 3; i < 32; i++) 318 | for (int j = 3; j < 32; j++) 319 | dct_data[i*32 + j] = 0; 320 | 321 | // idct 322 | plan = fftwf_plan_r2r_2d(DOWNSAMPLE_SIZE, DOWNSAMPLE_SIZE, dct_data, dct_data, FFTW_REDFT01, FFTW_REDFT01, FFTW_ESTIMATE); 323 | fftwf_execute(plan); 324 | fftwf_destroy_plan(plan); 325 | */ 326 | /*for (int i = 0; i < (int)(DOWNSAMPLE_SIZE*DOWNSAMPLE_SIZE); i++) 327 | data[i] = dct_data[i] / (4.0*DOWNSAMPLE_SIZE*DOWNSAMPLE_SIZE) + 127;*/ 328 | 329 | 330 | // Calculate mean frequency value of top 8x8 bins 331 | float* p = dct_data; 332 | float mean_pixel = 0; 333 | mean_pixel -= dct_data[0]; // ignore the DC component 334 | for (int i = 0; i < 8; i++) 335 | { 336 | for (int j = 0; j < 8; j++) 337 | mean_pixel += *(p++); 338 | 339 | p += 24; 340 | } 341 | 342 | mean_pixel /= 63; 343 | 344 | 345 | // Calculate image hash, by setting bits high if that frequency bin is higher than the mean. 346 | NSUInteger hash_value = 0; 347 | p = dct_data; 348 | for (int i = 0; i < 8; i++) 349 | { 350 | for (int j = 0; j < 8; j++) 351 | { 352 | if (*(p++) > mean_pixel) 353 | hash_value |= (1UL << (i*8+j)); 354 | } 355 | 356 | p += 24; 357 | } 358 | 359 | return [NSNumber numberWithUnsignedInteger:hash_value]; 360 | } 361 | 362 | - (void)writeBitmapContext:(CGContextRef)context toDebugFile:(NSString*)name 363 | { 364 | #if 0 365 | CGImageRef image_ref = CGBitmapContextCreateImage(context); 366 | NSImage* output_image = [[NSImage alloc] initWithCGImage:image_ref size:NSZeroSize]; 367 | CGImageRelease(image_ref); 368 | [[output_image TIFFRepresentation] writeToFile:[[NSString stringWithFormat:@"~/ShortTerm/sd-dbg %@.tif", name] stringByExpandingTildeInPath] atomically:NO]; 369 | #endif 370 | } 371 | 372 | 373 | 374 | 375 | 376 | @end 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | #pragma mark - 395 | #pragma mark Transforming hashes 396 | /* 397 | // From Hacker's Delight, 7-3, http://www.hackersdelight.org/HDcode/transpose8.c.txt 398 | static uint64_t transpose8b64(uint64_t x) 399 | { 400 | return (x & 0x8040201008040201LL) | 401 | (x & 0x0080402010080402LL) << 7 | 402 | (x & 0x0000804020100804LL) << 14 | 403 | (x & 0x0000008040201008LL) << 21 | 404 | (x & 0x0000000080402010LL) << 28 | 405 | (x & 0x0000000000804020LL) << 35 | 406 | (x & 0x0000000000008040LL) << 42 | 407 | (x & 0x0000000000000080LL) << 49 | 408 | ((x >> 7) & 0x0080402010080402LL) | 409 | ((x >> 14) & 0x0000804020100804LL) | 410 | ((x >> 21) & 0x0000008040201008LL) | 411 | ((x >> 28) & 0x0000000080402010LL) | 412 | ((x >> 35) & 0x0000000000804020LL) | 413 | ((x >> 42) & 0x0000000000008040LL) | 414 | ((x >> 49) & 0x0000000000000080LL) ; 415 | } 416 | 417 | 418 | // Rotate a hash. `degrees' must equal one of: 0, 90, 180, 270 419 | image_hash_t DJImageHashRotate(image_hash_t hash, NSInteger degrees) 420 | { 421 | image_hash_t new_hash = 0; 422 | 423 | switch (degrees) 424 | { 425 | case 0: 426 | return hash; 427 | 428 | case 90: 429 | return DJImageHashHorizontalFlip(transpose8b64(hash)); 430 | 431 | case 180: 432 | return DJImageHashVerticalFlip(DJImageHashHorizontalFlip(hash)); 433 | 434 | case 270: 435 | return DJImageHashVerticalFlip(transpose8b64(hash)); 436 | break; 437 | } 438 | 439 | return 0; 440 | } 441 | 442 | // Flip a hash. 443 | image_hash_t DJImageHashVerticalFlip(image_hash_t hash) 444 | { 445 | // Swap each byte in the hash 446 | return ((hash << 56) | 447 | ((hash << 40) & 0xff000000000000ULL) | 448 | ((hash << 24) & 0xff0000000000ULL) | 449 | ((hash << 8) & 0xff00000000ULL) | 450 | ((hash >> 8) & 0xff000000ULL) | 451 | ((hash >> 24) & 0xff0000ULL) | 452 | ((hash >> 40) & 0xff00ULL) | 453 | ((hash >> 56))); 454 | } 455 | 456 | image_hash_t DJImageHashHorizontalFlip(NSUInteger hash) 457 | { 458 | image_hash_t newHash = 0; 459 | 460 | // For each byte in hash, flip bits 461 | for (int i = 0; i < 64; i += 8) 462 | { 463 | uint8_t b = (hash >> i) & 0xff; 464 | b = (b * 0x0202020202ULL & 0x010884422010ULL) % 1023; 465 | newHash |= (unsigned long long)b << i; 466 | } 467 | 468 | return newHash; 469 | } 470 | 471 | 472 | #pragma mark Comparing Hashes 473 | 474 | // Bit distance between two hashes 475 | NSInteger DJImageHashCompare(image_hash_t hash1, image_hash_t hash2) 476 | { 477 | return (NSInteger)HAMMING_DISTANCE(hash1, hash2); 478 | } 479 | 480 | // Bit distance between two hashes; first image will be transformed using all available transforms; closest distance will be returned. 481 | NSInteger DJImageHashCompareWithTransforms(image_hash_t hash1, image_hash_t hash2) 482 | { 483 | image_hash_t alternateHashes[6]; 484 | 485 | alternateHashes[0] = hash1; 486 | alternateHashes[1] = DJImageHashVerticalFlip(hash1); 487 | alternateHashes[2] = DJImageHashHorizontalFlip(hash1); 488 | alternateHashes[3] = DJImageHashRotate(hash1, 90); 489 | alternateHashes[4] = DJImageHashRotate(hash1, 180); 490 | alternateHashes[5] = DJImageHashRotate(hash1, 270); 491 | 492 | int smallestDistance = 64, dist; 493 | for (int i = 0; i < (int)(sizeof(alternateHashes) / sizeof(image_hash_t)); i++) 494 | { 495 | dist = DJImageHashCompare(alternateHashes[i], hash2); 496 | 497 | if (dist < smallestDistance) 498 | smallestDistance = dist; 499 | } 500 | 501 | return smallestDistance; 502 | } 503 | 504 | 505 | */ 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | #if 0 517 | image_hash_t DJAverageImageHashFromURL(NSURL* imageURL) 518 | { 519 | static const size_t DOWNSAMPLE_SIZE = 8; 520 | static CGColorSpaceRef _gray_color_space; 521 | static CFDictionaryRef _image_source_options; 522 | static BOOL _initialized = NO; 523 | 524 | if (!_initialized) 525 | { 526 | _gray_color_space = CGColorSpaceCreateDeviceGray(); 527 | _image_source_options = (__bridge_retained CFDictionaryRef)[[NSDictionary alloc] initWithObjectsAndKeys: 528 | [NSNumber numberWithUnsignedInteger:DOWNSAMPLE_SIZE*2], kCGImageSourceThumbnailMaxPixelSize, /* double pixel resolution becuase thumbnail creator respects aspect ratio */ 529 | [NSNumber numberWithBool:NO], kCGImageSourceShouldCache, 530 | nil]; 531 | _initialized = YES; 532 | 533 | } 534 | 535 | CGImageSourceRef image_source = CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, _image_source_options); 536 | 537 | if (image_source == NULL) 538 | { 539 | NSLog(@"%s: couldn't load image source %@; file probably doesn't exist.", __func__, imageURL); 540 | return 0; 541 | } 542 | 543 | CGImageRef thumbnail_image = CGImageSourceCreateThumbnailAtIndex(image_source, 0, _image_source_options); 544 | 545 | if (thumbnail_image == NULL) 546 | thumbnail_image = CGImageSourceCreateImageAtIndex(image_source, 0, _image_source_options); 547 | 548 | CFRelease(image_source); 549 | 550 | if (thumbnail_image == NULL) 551 | { 552 | NSLog(@"%s: couldn't generate thumbnail. Image data is probably corrupt.", __func__); 553 | return 0; 554 | } 555 | 556 | // Create a gray 8x8 representation. 557 | uint8 data[DOWNSAMPLE_SIZE * DOWNSAMPLE_SIZE]; 558 | CGContextRef gray_bitmap_context = CGBitmapContextCreate(data, DOWNSAMPLE_SIZE, DOWNSAMPLE_SIZE, 8, DOWNSAMPLE_SIZE, _gray_color_space, kCGImageAlphaNone); 559 | CGContextSetInterpolationQuality(gray_bitmap_context, kCGInterpolationHigh); 560 | CGContextDrawImage(gray_bitmap_context, NSMakeRect(0, 0, DOWNSAMPLE_SIZE, DOWNSAMPLE_SIZE), thumbnail_image); 561 | CGImageRelease(thumbnail_image); 562 | 563 | // Calculate mean pixel value 564 | uint8* p = data; 565 | NSInteger mean_pixel = 0; 566 | for (NSInteger i = 0; i < (NSInteger)(DOWNSAMPLE_SIZE*DOWNSAMPLE_SIZE); i++) 567 | mean_pixel += *(p++); 568 | 569 | mean_pixel /= DOWNSAMPLE_SIZE*DOWNSAMPLE_SIZE; 570 | 571 | // Calculate image hash 572 | p = data; 573 | uint64_t hash_value = 0; 574 | for (NSInteger i = 0; i < (NSInteger)(DOWNSAMPLE_SIZE*DOWNSAMPLE_SIZE); i++) 575 | if ((NSInteger)(*(p++)) > mean_pixel) 576 | hash_value |= (1UL << i); 577 | 578 | /*// Write the image to a test file. 579 | CGImageRef image_ref = CGBitmapContextCreateImage(gray_bitmap_context); 580 | NSImage* output_image = [[NSImage alloc] initWithCGImage:image_ref size:NSZeroSize]; 581 | CGImageRelease(image_ref); 582 | [[output_image TIFFRepresentation] writeToFile:[@"~/ShortTerm/out.tif" stringByExpandingTildeInPath] atomically:NO];*/ 583 | 584 | CGContextRelease(gray_bitmap_context); 585 | return (hash_value == 0) ? 1 : hash_value; 586 | } 587 | #endif -------------------------------------------------------------------------------- /SimilarImages/DJImageTrawler.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | 7 | /* 8 | A class to efficiently deep-search a directory and generate info for all images. 9 | */ 10 | 11 | #import 12 | @class DJPersistentCache; 13 | @interface DJImageTrawler : NSObject { 14 | 15 | @private 16 | NSURL* _root; 17 | volatile int64_t _unprocessedImageCount, _processedImageCount; 18 | } 19 | 20 | @property (readonly) NSOperationQueue* processingQueue, * searchingQueue; 21 | @property (readonly) NSMutableArray* images; 22 | 23 | - (id)initWithURL:(NSURL*)root_directory; 24 | - (void)trawlImagesWithProgressBlock:(void(^)(NSDictionary*))block; 25 | - (void)addUnprocessedImage; 26 | 27 | + (DJPersistentCache*)hashCache; 28 | @end 29 | -------------------------------------------------------------------------------- /SimilarImages/DJImageTrawler.m: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | 7 | 8 | #import "DJImageTrawler.h" 9 | #import "DJImageHash.h" 10 | #import "DJPersistentCache.h" 11 | 12 | #import "SIAdditions.h" 13 | 14 | @interface DJImageTrawler () 15 | 16 | @property (readwrite) NSOperationQueue* processingQueue, * searchingQueue; 17 | @property (readwrite) NSMutableArray* images; 18 | 19 | @end 20 | 21 | #pragma mark - 22 | @interface DJDirectorySearchOperation : NSOperation 23 | @property DJImageTrawler* owner; 24 | @property NSURL* startURL; 25 | @end 26 | 27 | #pragma mark - 28 | @interface DJImageProcessingOperation : NSOperation 29 | @property DJImageTrawler* owner; 30 | @property NSURL* imageURL; 31 | @property NSDictionary* imageFileProperties; 32 | @property NSString* cacheKey; 33 | @end 34 | 35 | 36 | #pragma mark - 37 | 38 | @implementation DJImageTrawler 39 | 40 | @synthesize searchingQueue, processingQueue, images; 41 | 42 | + (DJPersistentCache*)hashCache 43 | { 44 | static DJPersistentCache* _hash_cache = nil; 45 | 46 | if (_hash_cache == nil) 47 | { 48 | @synchronized(self) 49 | { 50 | if (!_hash_cache) 51 | { 52 | NSURL* cacheURL = [[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject]; 53 | _hash_cache = [[DJPersistentCache alloc] initWithURL:[cacheURL URLByAppendingPathComponent:[NSString stringWithFormat:@"%@-hashCache.dat", self]]]; 54 | [_hash_cache setMaxEntryCount:100000]; 55 | } 56 | } 57 | } 58 | 59 | return _hash_cache; 60 | } 61 | 62 | - (id)initWithURL:(NSURL*)root_directory 63 | { 64 | if (!(self = [super init])) 65 | return nil; 66 | 67 | // Create the operation queues for this instance. 68 | [self setProcessingQueue:[[NSOperationQueue alloc] init]]; 69 | [self setSearchingQueue:[[NSOperationQueue alloc] init]]; 70 | [[self processingQueue] setSuspended:YES]; 71 | [[self processingQueue] setMaxConcurrentOperationCount:16]; 72 | [[self searchingQueue] setMaxConcurrentOperationCount:4]; 73 | [self setImages:[NSMutableArray array]]; 74 | _root = root_directory; 75 | return self; 76 | } 77 | 78 | - (void)trawlImagesWithProgressBlock:(void(^)(NSDictionary*))progress_block 79 | { 80 | // Add the first operation, a search on the root directory. 81 | NSDate* start = [NSDate date]; 82 | DJDirectorySearchOperation* root_search = [[DJDirectorySearchOperation alloc] init]; 83 | [root_search setOwner:self]; 84 | [root_search setStartURL:_root]; 85 | [[self searchingQueue] addOperation:root_search]; 86 | 87 | 88 | // Wait for all operations to finish. 89 | for (;;) 90 | { 91 | usleep(50000); 92 | BOOL finished_searching = [[[self searchingQueue] operations] count] == 0; 93 | 94 | if (finished_searching) 95 | [[self processingQueue] setSuspended:NO]; 96 | 97 | progress_block([NSDictionary dictionaryWithObjectsAndKeys: 98 | [NSNumber numberWithInteger:_unprocessedImageCount], @"fileCount", 99 | [NSNumber numberWithInteger:_processedImageCount], @"completeCount", 100 | [NSNumber numberWithBool:finished_searching], @"searchComplete", nil]); 101 | 102 | if (finished_searching && [[[self processingQueue] operations] count] == 0) 103 | break; 104 | 105 | usleep(200000); 106 | } 107 | 108 | [[self searchingQueue] waitUntilAllOperationsAreFinished]; 109 | [[self processingQueue] waitUntilAllOperationsAreFinished]; 110 | 111 | NSLog(@"Trawl took %f seconds.", [[NSDate date] timeIntervalSinceDate:start]); 112 | 113 | [[[self class] hashCache] performSelectorInBackground:@selector(writeToPersistentStore) withObject:nil]; 114 | } 115 | 116 | - (void)addUnprocessedImage 117 | { 118 | OSAtomicIncrement64Barrier(&_unprocessedImageCount); 119 | } 120 | 121 | - (void)addProcessedImage 122 | { 123 | OSAtomicIncrement64Barrier(&_processedImageCount); 124 | } 125 | 126 | @end 127 | 128 | #pragma mark - 129 | 130 | @implementation DJImageProcessingOperation 131 | 132 | @synthesize imageURL, imageFileProperties, cacheKey, owner; 133 | 134 | - (void)main 135 | { 136 | NSAssert([self imageURL] != nil, @"%@ needs an image to work with.", [self class]); 137 | 138 | NSMutableDictionary* newImageItem = [NSMutableDictionary dictionaryWithObjectsAndKeys:[self imageURL], @"url", nil]; 139 | 140 | //NSLog(@"Hashing %@ (%@)", [self imageURL], cacheKey); 141 | DJImageHash* hash = [[DJImageHash alloc] initWithURL:[self imageURL]]; 142 | [hash calculateHashWithTransforms:NO]; 143 | 144 | // Note to our owner that we're finished with the computationally intensive part. 145 | [[self owner] addProcessedImage]; 146 | 147 | 148 | // If the hash failed, don't add it to owner's images. 149 | if (hash == nil) 150 | { 151 | NSLog(@"unable to hash %@", [self imageURL]); 152 | return; 153 | } 154 | 155 | // Store this hash in the cache. 156 | [[DJImageTrawler hashCache] setObject:hash forKey:[self cacheKey]]; 157 | 158 | // Set the hash item, then add to owner's images. 159 | [newImageItem setObject:hash forKey:@"hash"]; 160 | 161 | @synchronized ([[self owner] images]) 162 | { 163 | [[[self owner] images] addObject:newImageItem]; 164 | } 165 | } 166 | 167 | @end 168 | 169 | #pragma mark - 170 | 171 | @implementation DJDirectorySearchOperation 172 | 173 | @synthesize startURL, owner; 174 | 175 | + (NSArray*)imageFileExtensions 176 | { 177 | static NSArray* _extensions = nil; 178 | 179 | if (_extensions == nil) 180 | _extensions = [NSArray arrayWithObjects:@"jpg", @"jpeg", @"bmp", @"gif", @"png", @"tif", @"tiff", @"jp2", @"psd", @"pdf", nil]; 181 | 182 | return _extensions; 183 | } 184 | 185 | - (void)main 186 | { 187 | NSAssert([self startURL] != nil, @"%@ needs a starting URL.", [self class]); 188 | 189 | NSError* enumeration_error = NULL; 190 | NSArray* children = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:[self startURL] includingPropertiesForKeys:[NSArray arrayWithObjects:NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLFileSizeKey, nil] options:(NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsPackageDescendants) error:&enumeration_error]; 191 | 192 | if (!children) 193 | { 194 | NSLog(@"%s: error: %@", __func__, enumeration_error); 195 | return; 196 | }; 197 | 198 | for (NSURL *child in children) 199 | { 200 | NSNumber* is_directory = nil; 201 | [child getResourceValue:&is_directory forKey:NSURLIsDirectoryKey error:NULL]; 202 | 203 | if ([is_directory boolValue]) 204 | { 205 | // Queue a directory search for this subdirectory. 206 | DJDirectorySearchOperation* subdir_search = [[DJDirectorySearchOperation alloc] init]; 207 | [subdir_search setOwner:[self owner]]; 208 | [subdir_search setStartURL:child]; 209 | [subdir_search setQueuePriority:NSOperationQueuePriorityHigh]; 210 | [[[self owner] searchingQueue] addOperation:subdir_search]; 211 | } 212 | else 213 | { 214 | // If this file is not an image, skip it. 215 | if (![[DJDirectorySearchOperation imageFileExtensions] containsObject:[[child pathExtension] lowercaseString]]) 216 | { 217 | //NSLog(@"Skipping non-image %@", child); 218 | continue; 219 | } 220 | 221 | // Check the cache before queueing this item. 222 | NSDictionary* fileInfo = [child resourceValuesForKeys:[NSArray arrayWithObjects:NSURLContentModificationDateKey, NSURLFileSizeKey, nil] error:NULL]; 223 | NSString* cacheKey = [[NSString stringWithFormat:@"%@-%f-%@-%d-1", [child path], [[fileInfo objectForKey:NSURLContentModificationDateKey] timeIntervalSince1970], [fileInfo objectForKey:NSURLFileSizeKey], [DJImageHash latestVersion]] sha1Digest]; 224 | NSNumber* hash = [[DJImageTrawler hashCache] objectForKey:cacheKey]; 225 | 226 | if (hash) 227 | { 228 | [[self owner] addUnprocessedImage]; 229 | 230 | @synchronized([[self owner] images]) 231 | { 232 | [[[self owner] images] addObject:[NSDictionary dictionaryWithObjectsAndKeys:hash, @"hash", child, @"url", nil]]; 233 | } 234 | 235 | [[self owner] addProcessedImage]; 236 | } 237 | else 238 | { 239 | // Queue a processing operation for this image. 240 | DJImageProcessingOperation* image_processor = [[DJImageProcessingOperation alloc] init]; 241 | [image_processor setOwner:[self owner]]; 242 | [image_processor setImageURL:child]; 243 | [image_processor setCacheKey:cacheKey]; 244 | 245 | [[[self owner] processingQueue] addOperation:image_processor]; 246 | [[self owner] addUnprocessedImage]; 247 | } 248 | } 249 | } 250 | } 251 | 252 | 253 | @end -------------------------------------------------------------------------------- /SimilarImages/DJPersistentCache.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | 7 | #import 8 | 9 | @interface DJPersistentCache : NSObject 10 | 11 | @property (readwrite, assign) NSInteger maxEntryCount; 12 | @property (readwrite, retain) NSURL* URL; 13 | 14 | 15 | - (id)initWithURL:(NSURL*)cacheURL; 16 | 17 | 18 | - (NSInteger)count; 19 | - (void)writeToPersistentStore; 20 | - (BOOL)writeToURL:(NSURL*)cacheURL error:(NSError**)error; 21 | 22 | 23 | - (id)objectForKey:(id)key; 24 | - (void)setObject:(id)obj forKey:(id)key; 25 | - (void)removeAllObjects; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /SimilarImages/DJPersistentCache.m: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | #import "DJPersistentCache.h" 7 | 8 | @interface DJPersistentCacheItem : NSObject 9 | @property (readwrite, retain) id value; 10 | @property (readwrite, assign) NSTimeInterval lastAccessed; 11 | @end 12 | 13 | 14 | #pragma mark - 15 | 16 | #define DJPersistentCacheLoadFactor 0.8 17 | 18 | @interface DJPersistentCache () 19 | @property (readwrite, retain) NSMutableDictionary* cachedObjects; 20 | @property (readwrite, retain) NSLock* cacheLock; 21 | @end 22 | 23 | #pragma mark - 24 | 25 | @implementation DJPersistentCache 26 | 27 | @synthesize cachedObjects, maxEntryCount, URL, cacheLock; 28 | 29 | - (id)init 30 | { 31 | return [self initWithURL:nil]; 32 | } 33 | 34 | // Designated initializer. 35 | - (id)initWithURL:(NSURL *)cacheURL 36 | { 37 | if (!(self = [super init])) 38 | return nil; 39 | 40 | if (cacheURL != nil) 41 | { 42 | @try { 43 | [self setCachedObjects:[NSKeyedUnarchiver unarchiveObjectWithFile:[cacheURL path]]]; 44 | [self setURL:cacheURL]; 45 | } 46 | @catch (NSException *exception) { 47 | NSLog(@"%s: unable to read cache file %@", __func__, cacheURL); 48 | } 49 | } 50 | 51 | if ([self cachedObjects] == nil) 52 | [self setCachedObjects:[NSMutableDictionary dictionary]]; 53 | 54 | [self setCacheLock:[[NSLock alloc] init]]; 55 | return self; 56 | } 57 | 58 | - (void)writeToPersistentStore 59 | { 60 | [self writeToURL:[self URL] error:NULL]; 61 | } 62 | 63 | - (BOOL)writeToURL:(NSURL*)cacheURL error:(NSError**)error 64 | { 65 | @try { 66 | [[self cacheLock] lock]; 67 | [NSKeyedArchiver archiveRootObject:[self cachedObjects] toFile:[cacheURL path]]; 68 | } 69 | @catch (NSException *exception) 70 | { 71 | NSLog(@"%s: error %@", __func__, [exception reason]); 72 | *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:0 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:NSURLErrorFailingURLErrorKey, cacheURL, nil]]; 73 | return NO; 74 | } 75 | @finally { 76 | [[self cacheLock] unlock]; 77 | } 78 | 79 | 80 | return YES; 81 | } 82 | 83 | - (NSInteger)count 84 | { 85 | return (NSInteger)[[self cachedObjects] count]; 86 | } 87 | 88 | - (id)objectForKey:(id)key 89 | { 90 | [[self cacheLock] lock]; 91 | DJPersistentCacheItem* item = [[self cachedObjects] objectForKey:key]; 92 | 93 | if (!item) 94 | { 95 | [[self cacheLock] unlock]; 96 | return nil; 97 | } 98 | 99 | [item setLastAccessed:[[NSDate date] timeIntervalSince1970]]; 100 | [[self cacheLock] unlock]; 101 | 102 | return [item value]; 103 | } 104 | 105 | - (void)setObject:(id)obj forKey:(id)key 106 | { 107 | [[self cacheLock] lock]; 108 | 109 | if ([self cacheCleanNeeded]) 110 | [self cleanCache]; 111 | 112 | DJPersistentCacheItem* newItem = [[DJPersistentCacheItem alloc] init]; 113 | [newItem setValue:obj]; 114 | [newItem setLastAccessed:[[NSDate date] timeIntervalSince1970]]; 115 | [[self cachedObjects] setObject:newItem forKey:key]; 116 | 117 | [[self cacheLock] unlock]; 118 | } 119 | 120 | - (void)removeAllObjects 121 | { 122 | [[self cacheLock] lock]; 123 | [[self cachedObjects] removeAllObjects]; 124 | [[self cacheLock] unlock]; 125 | } 126 | 127 | - (BOOL)cacheCleanNeeded 128 | { 129 | return ([self maxEntryCount] > 0) && ((NSInteger)[[self cachedObjects] count] >= [self maxEntryCount]); 130 | } 131 | 132 | - (void)cleanCache 133 | { 134 | // Create a sorted representation of all objects. 135 | NSMutableArray* items = [NSMutableArray arrayWithCapacity:[[self cachedObjects] count]]; 136 | 137 | for (id key in [[self cachedObjects] keyEnumerator]) 138 | [items addObject:[NSArray arrayWithObjects:key, [NSNumber numberWithDouble:[[[self cachedObjects] objectForKey:key] lastAccessed]], nil]]; 139 | 140 | [items sortUsingComparator:^NSComparisonResult(id obj1, id obj2) { 141 | return [[obj1 objectAtIndex:1] compare:[obj2 objectAtIndex:1]]; 142 | }]; 143 | 144 | // Remove the top 15 items. 145 | for (NSInteger i = 0, l = MIN((NSInteger)[items count], (NSInteger)([self maxEntryCount] * (1.0 - DJPersistentCacheLoadFactor))); i < l; i++) 146 | [[self cachedObjects] removeObjectForKey:[[items objectAtIndex:(NSUInteger)i] objectAtIndex:0]]; 147 | } 148 | 149 | 150 | @end 151 | 152 | #pragma mark - 153 | 154 | @implementation DJPersistentCacheItem 155 | 156 | @synthesize value, lastAccessed; 157 | 158 | - (id)initWithCoder:(NSCoder *)decoder 159 | { 160 | if (!(self = [self init])) 161 | return nil; 162 | 163 | [self setValue:[decoder decodeObjectForKey:@"v"]]; 164 | [self setLastAccessed:[decoder decodeDoubleForKey:@"d"]]; 165 | return self; 166 | } 167 | 168 | - (void)encodeWithCoder:(NSCoder *)encoder 169 | { 170 | [encoder encodeObject:[self value] forKey:@"v"]; 171 | [encoder encodeDouble:[self lastAccessed] forKey:@"d"]; 172 | } 173 | 174 | @end -------------------------------------------------------------------------------- /SimilarImages/DJPersistentCacheTest.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | 7 | #import 8 | 9 | @interface DJPersistentCacheTest : SenTestCase 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /SimilarImages/DJPersistentCacheTest.m: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | 7 | #import "DJPersistentCacheTest.h" 8 | 9 | #import "DJPersistentCache.h" 10 | 11 | @implementation DJPersistentCacheTest 12 | 13 | 14 | - (void)testCleaner 15 | { 16 | NSInteger maxCacheSize = 1000; 17 | DJPersistentCache* cache = [[DJPersistentCache alloc] init]; 18 | [cache setMaxEntryCount:maxCacheSize]; 19 | 20 | for (NSInteger i = 0; i < 10000; i++) 21 | { 22 | @autoreleasepool { 23 | NSNumber* n = [NSNumber numberWithInteger:i]; 24 | [cache setObject:n forKey:n]; 25 | } 26 | } 27 | 28 | STAssertTrue([cache count] <= maxCacheSize, @"%s: cache count should be less than max value %d, but is %d", __func__, maxCacheSize, [cache count]); 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /SimilarImages/SIAdditions.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | #import 7 | 8 | @interface NSString (DJHashingAdditions) 9 | - (NSString *)sha1Digest; 10 | @end 11 | 12 | NSString* DJHexadecimalStringFromBytes(const unsigned char* data, NSUInteger dataLength); 13 | -------------------------------------------------------------------------------- /SimilarImages/SIAdditions.m: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | /* Copyright 2007-2009 Dorian Johnson; created 2007-06-10 (DJHexadecimalStringFromBytes, sha1Digest) */ 7 | 8 | 9 | #import "SIAdditions.h" 10 | #import 11 | 12 | NSString* DJHexadecimalStringFromBytes(const unsigned char* data, NSUInteger dataLength) 13 | { 14 | static unsigned char hexDigits[] = "0123456789abcdef"; 15 | unsigned char* output = malloc(dataLength*2+1); 16 | 17 | for (NSUInteger i = 0; i < dataLength; i++) 18 | { 19 | output[2*i] = hexDigits[data[i] >> 4]; 20 | output[2*i+1] = hexDigits[data[i] & 0x0f]; 21 | } 22 | 23 | output[dataLength] = '\0'; 24 | 25 | NSString *finishedString = [NSString stringWithCString:(const char *)output encoding:NSASCIIStringEncoding]; 26 | free(output); 27 | return finishedString; 28 | } 29 | 30 | 31 | @implementation NSString (DJHashingAdditions) 32 | 33 | - (NSString *)sha1Digest 34 | { 35 | unsigned char hashedChars[20]; 36 | CC_SHA1([self UTF8String], (CC_LONG)[self lengthOfBytesUsingEncoding:NSUTF8StringEncoding], hashedChars); 37 | return DJHexadecimalStringFromBytes(hashedChars, 20); 38 | } 39 | 40 | 41 | @end 42 | 43 | -------------------------------------------------------------------------------- /SimilarImages/SIAppDelegate.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | #import 7 | 8 | @interface SIAppDelegate : NSObject 9 | 10 | 11 | - (IBAction)clearHashCache:(id)sender; 12 | @end 13 | -------------------------------------------------------------------------------- /SimilarImages/SIAppDelegate.m: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | #import "SIAppDelegate.h" 7 | 8 | // To clear the hash cache: 9 | #import "DJImageTrawler.h" 10 | #import "DJPersistentCache.h" 11 | 12 | @implementation SIAppDelegate 13 | 14 | - (void)applicationDidFinishLaunching:(NSNotification *)notification 15 | { 16 | 17 | // NSUserDefaults defaults 18 | [[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"SIDefaults" ofType:@"plist"]]]; 19 | } 20 | 21 | 22 | 23 | - (IBAction)clearHashCache:(id)sender 24 | { 25 | [[DJImageTrawler hashCache] removeAllObjects]; 26 | [[DJImageTrawler hashCache] writeToPersistentStore]; 27 | 28 | NSRunAlertPanel(@"The image cache is now empty.", @"This usually shouldn't be necessary: image changes are automatically detected, and the cache uses a very small amount of disk space.", @"OK", @"", @""); 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /SimilarImages/SIAppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorianj/SimilarImages/8d95c8783f46cc28b4379e77903140d80617e970/SimilarImages/SIAppIcon.icns -------------------------------------------------------------------------------- /SimilarImages/SIDefaults.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SISearchSensitivity 6 | 5 7 | 8 | 9 | -------------------------------------------------------------------------------- /SimilarImages/SIDocument.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | #import 7 | #import 8 | 9 | @class SIImageView; 10 | 11 | @interface SIDocument : NSDocument 12 | 13 | // The root URL for this search 14 | @property (readwrite, retain) NSURL* rootURL; 15 | 16 | // Chooser for the search path 17 | @property (readwrite, assign) IBOutlet NSPathControl* haystackPathControl; 18 | 19 | // An image view that is used to pick the needle image. 20 | @property (readwrite, assign) IBOutlet SIImageView* needleImagePicker; 21 | 22 | // Thumbnail view used for image matches 23 | @property (readwrite, assign) IBOutlet IKImageBrowserView* resultsImageBrowserView; 24 | 25 | // Images returned from search. 26 | @property (readwrite, retain) NSArray* matchingImages; 27 | 28 | // For the trawling progress sheet 29 | @property (readwrite, assign) IBOutlet NSWindow* trawlProgressWindow; 30 | @property (readwrite, assign) IBOutlet NSTextField* trawlProgressImageCount; 31 | @property (readwrite, assign) IBOutlet NSProgressIndicator* trawlProgressIndicator; 32 | 33 | 34 | // Sent when a user drops an image onto the needle image picker. 35 | - (IBAction)userDidDropImage:(id)sender; 36 | 37 | // Runs the search dir chooser and re-scans 38 | - (IBAction)runRootDirChooserSheet:(id)sender; 39 | 40 | // Actions on search results 41 | - (IBAction)revealSearchResultInFinder:(id)sender; 42 | - (IBAction)openSearchResultWithDefaultApp:(id)sender; 43 | @end 44 | -------------------------------------------------------------------------------- /SimilarImages/SIDocument.m: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | #import "SIDocument.h" 7 | #import "SIImageBrowserItem.h" 8 | 9 | #import "SIImageView.h" 10 | #import "DJImageTrawler.h" 11 | #import "DJImageHash.h" 12 | 13 | 14 | #define TRACE_FUNC() NSLog(@"%s", __func__) 15 | 16 | @interface SIDocument () 17 | 18 | @property (readwrite, retain) NSArray* observedKeys; 19 | 20 | // An array of all found images. 21 | @property (readwrite, retain) NSArray* images; 22 | 23 | // The image last searched for. 24 | @property (readwrite, retain) DJImageHash* needleImageHash; 25 | 26 | @end 27 | 28 | 29 | @implementation SIDocument 30 | 31 | // Public properties. 32 | @synthesize needleImagePicker, matchingImages, resultsImageBrowserView, rootURL, haystackPathControl; 33 | 34 | // Private properties. 35 | @synthesize images, observedKeys, needleImageHash; 36 | 37 | 38 | 39 | - (id)init 40 | { 41 | if ((self = [super init]) == nil) 42 | return nil; 43 | 44 | [self setObservedKeys:[NSArray arrayWithObjects: 45 | @"matchingImages", // Update the browser when the results change 46 | @"rootURL", // When rootURL is changed, scan the new one 47 | @"needleImageHash", // When search URL is changed, re-search 48 | nil]]; 49 | 50 | for (NSString* keyPath in [self observedKeys]) 51 | [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; 52 | 53 | [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"SISearchSensitivity" options:NSKeyValueObservingOptionNew context:NULL]; 54 | return self; 55 | } 56 | 57 | - (void)dealloc 58 | { 59 | for (NSString* keyPath in [self observedKeys]) 60 | [self removeObserver:self forKeyPath:keyPath]; 61 | 62 | [[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:@"SISearchSensitivity"]; 63 | } 64 | 65 | - (void)awakeFromNib 66 | { 67 | 68 | 69 | // Configure the results image browser 70 | [[self resultsImageBrowserView] setCanControlQuickLookPanel:YES]; 71 | [[self resultsImageBrowserView] setCellsStyleMask:(IKCellsStyleNone | IKCellsStyleShadowed | IKCellsStyleTitled /*| IKCellsStyleSubtitled*/)]; 72 | 73 | [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 74 | [self runRootDirChooserSheet:nil]; 75 | }]; 76 | } 77 | 78 | - (IBAction)runRootDirChooserSheet:(id)sender 79 | { 80 | NSOpenPanel* open_panel = [NSOpenPanel openPanel]; 81 | [open_panel setCanChooseDirectories:YES]; 82 | [open_panel setCanChooseFiles:NO]; 83 | 84 | if ([sender respondsToSelector:@selector(clickedPathComponentCell)]) 85 | [open_panel setDirectoryURL:[[sender clickedPathComponentCell] URL]];; 86 | 87 | [open_panel beginSheetModalForWindow:[self windowForSheet] completionHandler:^(NSInteger result) { 88 | if (result != NSOKButton) 89 | { 90 | if ([self rootURL] == nil) 91 | { 92 | // Didnt' have a root URL previously; showing the window now would leave it unusable. 93 | [self close]; 94 | } 95 | 96 | return; 97 | } 98 | 99 | [self setRootURL:[open_panel URL]]; 100 | }]; 101 | } 102 | 103 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 104 | { 105 | if (object == self) 106 | { 107 | if ([keyPath isEqualToString:@"matchingImages"]) 108 | { 109 | [[self resultsImageBrowserView] performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO]; 110 | } 111 | else if ([keyPath isEqualToString:@"rootURL"]) 112 | { 113 | if ([self rootURL] == nil) 114 | return; 115 | 116 | NSString* windowTitle = [[self rootURL] lastPathComponent]; 117 | [self setDisplayName:windowTitle]; 118 | [[self windowForSheet] setTitle:windowTitle]; 119 | [[self rootURL] startAccessingSecurityScopedResource]; 120 | [self performSelectorOnMainThread:@selector(trawlRootURL) withObject:nil waitUntilDone:NO]; 121 | } 122 | else if ([keyPath isEqualToString:@"needleImageHash"]) 123 | { 124 | if ([self needleImageHash] == nil) 125 | return; 126 | 127 | [self setMatchingImages:[self findImagesVisuallySimilarToImage:[self needleImageHash]]]; 128 | 129 | if ([keyPath isEqualToString:@"needleImageHash"] && ([[self matchingImages] count] == 0)) 130 | NSRunAlertPanel(@"No images found", @"No similar images were found. Try reducing sensitivity to get more results.", @"OK", @"", @""); 131 | } 132 | } 133 | else if (object == [NSUserDefaults standardUserDefaults]) 134 | { 135 | if ([keyPath isEqualToString:@"SISearchSensitivity"]) 136 | { 137 | [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(searchSensitivityChanged) object:nil]; 138 | [self performSelector:@selector(searchSensitivityChanged) withObject:nil afterDelay:0.25]; 139 | } 140 | } 141 | } 142 | 143 | - (void)searchSensitivityChanged 144 | { 145 | if ([self needleImageHash] == nil) 146 | return; 147 | 148 | BOOL changed = NO; 149 | NSArray* newMatches = [self findImagesVisuallySimilarToImage:[self needleImageHash]]; 150 | 151 | if ([newMatches count] != [[self matchingImages] count]) 152 | changed = YES; 153 | else 154 | { 155 | for (NSUInteger i = 0; i < [[self matchingImages] count]; i++) 156 | if (![[[[self matchingImages] objectAtIndex:i] objectForKey:@"bitem"] isEqual:[[newMatches objectAtIndex:i] objectForKey:@"bitem"]]) 157 | { 158 | changed = YES; 159 | break; 160 | } 161 | } 162 | 163 | if (changed) 164 | [self setMatchingImages:newMatches]; 165 | } 166 | 167 | 168 | #pragma mark - 169 | #pragma mark Managing the trawl progress sheet 170 | 171 | @synthesize trawlProgressWindow, trawlProgressIndicator, trawlProgressImageCount; 172 | 173 | - (void)showTrawlProgressSheet 174 | { 175 | [[NSApplication sharedApplication] beginSheet:[self trawlProgressWindow] modalForWindow:[self windowForSheet] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:NULL]; 176 | [[self trawlProgressIndicator] startAnimation:self]; 177 | } 178 | 179 | - (void)hideTrawlProgressSheet 180 | { 181 | [[NSApplication sharedApplication] endSheet:[self trawlProgressWindow]]; 182 | } 183 | 184 | - (void)updateTrawlProgressSheet:(NSDictionary*)trawl_info 185 | { 186 | if ([[trawl_info objectForKey:@"searchComplete"] boolValue]) 187 | { 188 | if ([[self trawlProgressIndicator] isIndeterminate]) 189 | { 190 | [[self trawlProgressIndicator] setIndeterminate:NO]; 191 | [[self trawlProgressIndicator] setMinValue:0]; 192 | } 193 | 194 | [[self trawlProgressIndicator] setMaxValue:[[trawl_info objectForKey:@"fileCount"] doubleValue]]; 195 | [[self trawlProgressIndicator] setDoubleValue:[[trawl_info objectForKey:@"completeCount"] doubleValue]]; 196 | [[self trawlProgressIndicator] displayIfNeeded]; 197 | } 198 | else 199 | { 200 | if (![[self trawlProgressIndicator] isIndeterminate]) 201 | { 202 | [[self trawlProgressIndicator] setIndeterminate:YES]; 203 | } 204 | } 205 | 206 | [[self trawlProgressImageCount] setStringValue:[NSString stringWithFormat:@"%ld / %ld", [[trawl_info objectForKey:@"completeCount"] integerValue], [[trawl_info objectForKey:@"fileCount"] integerValue]]]; 207 | } 208 | 209 | - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo 210 | { 211 | [sheet close]; 212 | } 213 | 214 | #pragma mark - 215 | #pragma mark NSDocument 216 | 217 | - (NSString *)windowNibName 218 | { 219 | return @"SIDocument"; 220 | } 221 | 222 | - (void)windowControllerDidLoadNib:(NSWindowController *)aController 223 | { 224 | [super windowControllerDidLoadNib:aController]; 225 | // Add any code here that needs to be executed once the windowController has loaded the document's window. 226 | } 227 | 228 | + (BOOL)autosavesInPlace 229 | { 230 | return YES; 231 | } 232 | 233 | - (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError 234 | { 235 | if (![typeName isEqualToString:@"SimilarImagesSearch"]) 236 | { 237 | NSLog(@"%s doesn't support data of type %@", __func__, typeName); 238 | return nil; 239 | } 240 | 241 | // Pack up the root URL in a secure bookmark. 242 | NSError* error; 243 | NSData* rootURLData = [[self rootURL] bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; 244 | 245 | if (rootURLData == nil) 246 | { 247 | NSLog(@"%s: Error saving: couldn't create data of rootURL. %@", __func__, error); 248 | return nil; 249 | } 250 | 251 | NSMutableDictionary* searchInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: 252 | // [self images], @"images", 253 | rootURLData, @"rootURL", 254 | nil]; 255 | 256 | //return [NSPropertyListSerialization dataWithPropertyList:archivedSearchInfo format:NSPropertyListBinaryFormat_v1_0 options:NSPropertyListImmutable error:NULL]; 257 | return [NSKeyedArchiver archivedDataWithRootObject:searchInfo]; 258 | } 259 | 260 | - (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError 261 | { 262 | if (![typeName isEqualToString:@"SimilarImagesSearch"]) 263 | { 264 | NSLog(@"%s doesn't support data of type %@", __func__, typeName); 265 | return NO; 266 | } 267 | 268 | NSDictionary* searchInfo = [NSKeyedUnarchiver unarchiveObjectWithData:data]; 269 | 270 | NSError* error; 271 | BOOL bookmarkIsStale; 272 | NSURL* newRootURL = [NSURL URLByResolvingBookmarkData:[searchInfo objectForKey:@"rootURL"] options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&bookmarkIsStale error:&error]; 273 | 274 | if (newRootURL == nil) 275 | { 276 | NSLog(@"%s: Error loading: couldn't create URL from data. %@", __func__, error); 277 | return NO; 278 | } 279 | 280 | [self setRootURL:newRootURL]; 281 | return YES; 282 | } 283 | 284 | 285 | #pragma mark - 286 | #pragma mark Searching directories for images 287 | 288 | - (void)trawlRootURL 289 | { 290 | [self showTrawlProgressSheet]; 291 | 292 | [[NSOperationQueue new] addOperationWithBlock:^{ 293 | [self setImages:[self trawlImagesInURL:[self rootURL]]]; 294 | [self performSelectorOnMainThread:@selector(hideTrawlProgressSheet) withObject:nil waitUntilDone:NO]; 295 | }]; 296 | } 297 | 298 | - (NSArray*)trawlImagesInURL:(NSURL*)root_url 299 | { 300 | DJImageTrawler* trawler = [[DJImageTrawler alloc] initWithURL:root_url]; 301 | [trawler trawlImagesWithProgressBlock:^(NSDictionary* progress_info) { 302 | [self performSelectorOnMainThread:@selector(updateTrawlProgressSheet:) withObject:progress_info waitUntilDone:NO]; 303 | }]; 304 | return [trawler images]; 305 | } 306 | 307 | 308 | #pragma mark - 309 | #pragma mark Finding visually similar images (after trawling) 310 | 311 | - (NSArray*)findImagesVisuallySimilarToImage:(DJImageHash*)needleHash 312 | { 313 | NSMutableArray* matches = [NSMutableArray array]; 314 | NSNumber* sensitivity = [[NSUserDefaults standardUserDefaults] objectForKey:@"SISearchSensitivity"]; 315 | [needleHash calculateHashWithTransforms:YES]; 316 | 317 | [[self images] enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 318 | NSDictionary* item = obj; 319 | 320 | NSNumber* similarity = [needleHash similarityTo:[item objectForKey:@"hash"] considerTransforms:YES]; 321 | 322 | //NSLog(@"Considering %@ (%@ match)", [[item objectForKey:@"url"] lastPathComponent], similarity); 323 | 324 | if (!similarity || ([sensitivity compare:similarity] == NSOrderedDescending) ) 325 | return; 326 | 327 | //NSLog(@"Found match %@: %@", similarity, [[item objectForKey:@"url"] lastPathComponent]); 328 | 329 | 330 | // This image is a match: create a browser item for it and add to the matches builder array. 331 | SIImageBrowserItem* browser_item = [[SIImageBrowserItem alloc] init]; 332 | [browser_item setImageURL:[item objectForKey:@"url"]]; 333 | 334 | @synchronized (matches) 335 | { 336 | [matches addObject:[NSDictionary dictionaryWithObjectsAndKeys:browser_item, @"bitem", similarity, @"dist", nil]]; 337 | } 338 | }]; 339 | 340 | [matches sortUsingDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"dist" ascending:NO]]]; 341 | return matches; 342 | } 343 | 344 | - (IBAction)userDidDropImage:(id)sender 345 | { 346 | DJImageHash* hash = [[DJImageHash alloc] init]; 347 | 348 | if ([[self needleImagePicker] imageURL] != nil) 349 | [hash setURL:[[self needleImagePicker] imageURL]]; 350 | 351 | else if ([[self needleImagePicker] image] != nil) 352 | [hash setImage:[[self needleImagePicker] image]]; 353 | else 354 | hash = nil; 355 | 356 | [self setNeedleImageHash:hash]; 357 | } 358 | 359 | #pragma mark - 360 | #pragma mark Working with the search results 361 | 362 | - (NSArray*)selectedResultImageURLs 363 | { 364 | NSIndexSet* selectedIndices = [[self resultsImageBrowserView] selectionIndexes]; 365 | 366 | if ([selectedIndices count] == 0) 367 | return nil; 368 | 369 | NSMutableArray* selectedURLs = [NSMutableArray array]; 370 | 371 | [selectedIndices enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { 372 | [selectedURLs addObject:[[[[self matchingImages] objectAtIndex:idx] objectForKey:@"bitem"] imageURL]]; 373 | }]; 374 | 375 | return selectedURLs; 376 | } 377 | 378 | - (IBAction)revealSearchResultInFinder:(id)sender 379 | { 380 | if ([self selectedResultImageURLs]) 381 | [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:[self selectedResultImageURLs]]; 382 | } 383 | 384 | - (IBAction)openSearchResultWithDefaultApp:(id)sender 385 | { 386 | //[[NSWorkspace sharedWorkspace] openURL:[[self selectedResultImageURLs] lastObject]]; 387 | [[NSWorkspace sharedWorkspace] openURLs:[self selectedResultImageURLs] withAppBundleIdentifier:@"com.apple.preview" options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifiers:NULL]; 388 | } 389 | 390 | 391 | #pragma mark - 392 | #pragma mark Working with the image browser (IKImageBrowserViewDataSource) 393 | 394 | - (NSUInteger)numberOfItemsInImageBrowser:(IKImageBrowserView*)browser; 395 | { 396 | return [[self matchingImages] count]; 397 | } 398 | 399 | - (id)imageBrowser:(IKImageBrowserView *)browser itemAtIndex:(NSUInteger)index; 400 | { 401 | return [[[self matchingImages] objectAtIndex:index] objectForKey:@"bitem"]; 402 | } 403 | 404 | 405 | @end 406 | -------------------------------------------------------------------------------- /SimilarImages/SIImageBrowserItem.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | #import 7 | 8 | @interface SIImageBrowserItem : NSObject 9 | @property (readwrite) NSURL* imageURL; 10 | @end 11 | -------------------------------------------------------------------------------- /SimilarImages/SIImageBrowserItem.m: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | #import "SIImageBrowserItem.h" 7 | #import 8 | 9 | @implementation SIImageBrowserItem 10 | @synthesize imageURL; 11 | 12 | - (NSString*)imageUID 13 | { 14 | return [[self imageURL] absoluteString]; 15 | } 16 | 17 | - (NSString*)imageRepresentationType 18 | { 19 | return IKImageBrowserNSURLRepresentationType; 20 | } 21 | 22 | - (id)imageRepresentation 23 | { 24 | return [self imageURL]; 25 | } 26 | 27 | - (NSString*)imageTitle 28 | { 29 | return [[self imageURL] lastPathComponent]; 30 | } 31 | 32 | - (NSString*)imageSubtitle 33 | { 34 | return [[self imageURL] path]; 35 | } 36 | 37 | - (BOOL)isEqual:(id)object 38 | { 39 | if ([object respondsToSelector:@selector(imageUID)]) 40 | return [[self imageUID] isEqual:[object imageUID]]; 41 | 42 | return NO; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /SimilarImages/SIImageView.h: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | #import 7 | 8 | @interface SIImageView : NSImageView 9 | @property (readwrite) NSURL* imageURL; 10 | @end 11 | -------------------------------------------------------------------------------- /SimilarImages/SIImageView.m: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2012 Dorian Johnson <2012@dorianj.net> 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | #import "SIImageView.h" 7 | 8 | @implementation SIImageView 9 | @synthesize imageURL; 10 | 11 | - (void)concludeDragOperation:(id < NSDraggingInfo >)sender 12 | { 13 | NSPasteboard* pboard = [sender draggingPasteboard]; 14 | 15 | //if ([pboard 16 | 17 | 18 | 19 | NSArray* objs = [pboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:nil]; 20 | NSURL* draggedURL = [objs lastObject]; 21 | /* 22 | if (draggedURL == nil) 23 | { 24 | for (id obj in [pboard propertyListForType:NSURLPboardType]) 25 | { 26 | if (![obj isKindOfClass:[NSString class]] || ([obj length] == 0)) 27 | continue; 28 | 29 | NSURL* url = [NSURL URLWithString:obj]; 30 | 31 | if (url != nil) 32 | { 33 | draggedURL = url; 34 | break; 35 | } 36 | } 37 | }*/ 38 | 39 | [self setImageURL:draggedURL]; 40 | [super concludeDragOperation:sender]; 41 | } 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /SimilarImages/SimilarImages-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeExtensions 11 | 12 | simim 13 | 14 | CFBundleTypeIconFile 15 | 16 | CFBundleTypeName 17 | SimilarImagesSearch 18 | LSHandlerRank 19 | Default 20 | CFBundleTypeOSTypes 21 | 22 | ???? 23 | 24 | CFBundleTypeRole 25 | Editor 26 | NSDocumentClass 27 | SIDocument 28 | 29 | 30 | CFBundleExecutable 31 | ${EXECUTABLE_NAME} 32 | CFBundleIconFile 33 | SIAppIcon.icns 34 | CFBundleIdentifier 35 | net.ahjia.SimilarImages 36 | CFBundleInfoDictionaryVersion 37 | 6.0 38 | CFBundleName 39 | ${PRODUCT_NAME} 40 | NSSupportsSuddenTermination 41 | 42 | CFBundlePackageType 43 | APPL 44 | CFBundleShortVersionString 45 | 1.0 46 | CFBundleSignature 47 | simi 48 | CFBundleVersion 49 | 1 50 | LSApplicationCategoryType 51 | public.app-category.photography 52 | LSMinimumSystemVersion 53 | ${MACOSX_DEPLOYMENT_TARGET} 54 | NSHumanReadableCopyright 55 | Copyright © 2012 Ahjia LLC. All rights reserved. 56 | NSMainNibFile 57 | MainMenu 58 | NSPrincipalClass 59 | NSApplication 60 | 61 | 62 | -------------------------------------------------------------------------------- /SimilarImages/SimilarImages-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'SimilarImages' target in the 'SimilarImages' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /SimilarImages/SimilarImages.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.files.bookmarks.document-scope 10 | 11 | com.apple.security.files.bookmarks.app-scope 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SimilarImages/browse_button_template.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorianj/SimilarImages/8d95c8783f46cc28b4379e77903140d80617e970/SimilarImages/browse_button_template.pdf -------------------------------------------------------------------------------- /SimilarImages/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /SimilarImages/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /SimilarImages/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | int main(int argc, char *argv[]) 4 | { 5 | return NSApplicationMain(argc, (const char **)argv); 6 | } 7 | -------------------------------------------------------------------------------- /Support/AppIcon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorianj/SimilarImages/8d95c8783f46cc28b4379e77903140d80617e970/Support/AppIcon.psd -------------------------------------------------------------------------------- /Support/browse_button_template.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorianj/SimilarImages/8d95c8783f46cc28b4379e77903140d80617e970/Support/browse_button_template.ai --------------------------------------------------------------------------------