├── .aspell.pws ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .yardopts ├── COPYING ├── Changelog ├── Gemfile ├── README.md ├── Rakefile ├── TODO ├── Tutorial.md ├── examples ├── copy_icc_profile.rb ├── external_photo.rb ├── print_portraits.rb ├── shift_time.rb └── show_speedup_with_fast_option.rb ├── lib └── mini_exiftool.rb ├── mini_exiftool.gemspec ├── regtest ├── read_all.rb └── read_all.yml └── test ├── data ├── Bad_PreviewIFD.jpg ├── Canon.jpg ├── INFORMATION ├── invalid_byte_sequence_in_utf8.json ├── invalid_rational.json ├── test.jpg ├── test.jpg.json ├── test_coordinates.jpg ├── test_encodings.jpg └── test_special_dates.jpg ├── helpers_for_test.rb ├── test_bad_preview_ifd.rb ├── test_class_methods.rb ├── test_composite.rb ├── test_copy_tags_from.rb ├── test_dumping.rb ├── test_encodings.rb ├── test_filename_access.rb ├── test_from_hash.rb ├── test_instance_methods.rb ├── test_invalid_byte_sequence_in_utf8.rb ├── test_invalid_rational.rb ├── test_io.rb ├── test_pathname.rb ├── test_pstore.rb ├── test_read.rb ├── test_read_coordinates.rb ├── test_read_numerical.rb ├── test_save.rb ├── test_special.rb ├── test_special_dates.rb └── test_write.rb /.aspell.pws: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janfri/mini_exiftool/cfb221a06440182a10d7fbafc29d5de0a1e1bca0/.aspell.pws -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | ruby-version: 10 | - '3.0' 11 | - '3.1' 12 | - '3.2' 13 | - '3.3' 14 | - 'ruby-head' 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: ${{ matrix.ruby-version }} 21 | - name: Install Exiftool 22 | run: | 23 | curl -L http://cpanmin.us | perl - --sudo Image::ExifTool 24 | export PATH=/usr/local/bin:$PATH 25 | exiftool -ver 26 | - name: Install Dependencies 27 | run: | 28 | bundle install 29 | - name: Run Tests 30 | run: | 31 | rake 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*.swp 3 | .regtestrc 4 | .rimrc 5 | .ruby-version 6 | .yardoc 7 | Gemfile.lock 8 | doc 9 | pkg 10 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --files Tutorial.rdoc,examples/*.rb 2 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | 2.14.0 2 | - Change default pstore_dir to subdir in Gem.cache_home or under USERPROFILE 3 | or Dir.tmpdir if some not defined. So it's now XDG conform and also Ruby 4 | ecosystem conform. Thanks a lot to Ryan Lue again for his effort and 5 | support (github issue #51). 6 | - Add the possibility to change the pstore_dir by setting the environment 7 | variable MINI_EXIFTOOL_PSTORE_DIR. 8 | 9 | 2.13.1 10 | - Second try to fix bug when using Pathname instances of filenames. 11 | 12 | 2.13.0 13 | - Fix bug when using Pathname instances of filenames. 14 | Thanks Ryan Lue for reporting this problem (github issue #50). 15 | 16 | 2.12.1 17 | - Maintenance release: Optimizing some internals. 18 | 19 | 2.12.0 20 | - Make mini_exiftool Ruby 3.5 ready. 21 | - Minimize gem file size. 22 | 23 | 2.11.0 24 | - Fix an error with weird timestamps. 25 | Thanks to philister for fixing it (github pull request #45). 26 | - Set frozen_string_literal to false to work with newer Ruby versions. 27 | 28 | 2.10.4 29 | - Maintenance release. 30 | 31 | 2.10.3 32 | - Use YAML.unsafe_load and backport it if necessary. 33 | 34 | 2.10.2 35 | - Escape backticks and use escaped values in option coord_format. 36 | 37 | 2.10.1 38 | - Make access via PStore thread-safe. 39 | - Fix conversion of values with leading zeros. 40 | Thanks to jvradelis for reporting this bug (github issue #40) and the hint 41 | to the incorrect regular expression. 42 | 43 | 2.10.0 44 | - Implement the method MiniExiftool#respond_to_missing? for completeness and 45 | the possibility to use MiniExiftool with Forwardable. 46 | 47 | 2.9.1 48 | - Fix a problem with frozen string in newer Ruby versions. 49 | - Improve internal stuff and documentation. 50 | 51 | 2.9.0 52 | - Use rim/regtest instead of regtest/task to include regtest files into gem. 53 | - Update to rim 2.15 to include Gemfile and other stuff into gem file. 54 | 55 | 2.8.2 56 | - Upgrade to rim 2.10.0 to unfreeze gemspec. 57 | 58 | 2.8.1 59 | - Improve documentation and use Integer instead of Fixnum because it's 60 | deprecated in Ruby 2.4.0. 61 | 62 | 2.8.0 63 | - MiniExiftool doesn't close any longer a given IO to MiniExiftool.new 64 | respectively MiniExiftool#load. You are responsible to do that. 65 | Thanks to mfo for the suggestion and implementing it. 66 | 67 | 2.7.6 68 | - Add Gemfile (for those who use bundler). 69 | 70 | 2.7.5 71 | - Add gem description and fix typo in summary. 72 | 73 | 2.7.4 74 | - Print messages at $stdout when generating the PStore file with ExifTool tag names. 75 | Thanks to Joshfindit for the idea. 76 | 77 | 2.7.3 78 | - Update docs. 79 | 80 | 2.7.2 81 | - Include example files in gem. 82 | 83 | 2.7.1 84 | - Improve test for fast options: Ignore other messages from stderr. 85 | - Fix typo in license identifier. 86 | - Improve example show_speedup_with_fast_option. 87 | 88 | 2.7.0 89 | - Use duck typing to determine if filename_or_io is a filename or an IO 90 | instance. 91 | - New option :fast2. 92 | - Add example show_speedup_with_fast_option. 93 | - Update docs. 94 | 95 | 2.6.0 96 | - Support reading from IO instances. 97 | Thanks to Gaelan for the idea. 98 | - New option :fast to increase speed when extracting information from JPEG 99 | images which are piped across a slow network connection. 100 | Thanks to Felipe Cypriano for the idea. 101 | - Refactoring: Use Open3 for all command-line calls. 102 | 103 | 2.5.1 104 | - Add gemspec. 105 | 106 | 2.5.0 107 | - Make the pstore dir customizable: 108 | MiniExiftool.pstore_dir and MiniExiftool.pstore_dir= 109 | Thanks to Shawn Pyle for the idea and a first approach 110 | of implementation. 111 | - Update README. 112 | 113 | 2.4.2 114 | - Bugfix: Don't ignoring *_encoding options when saving. 115 | 116 | 2.4.1 117 | - Handling tag values of the form x/0 correct. 118 | Thanks to Picturelife for a clever solution to solve this. 119 | - Some internal housekeeping. 120 | 121 | 2.4.0 122 | - New method MiniExiftool#copy_tags_from. 123 | Many thanks to cgat for the hint and implementing a first 124 | approach. 125 | - Saver handling of encoding problems. 126 | - Approving documentation. 127 | - Using regtest for integration testing. 128 | - Some internal refactorings. 129 | 130 | 2.3.0 131 | - New options :exif_encoding, :iptc_encodings, 132 | :xmp_encodings etc. to change specific encodings 133 | See -charset option of the exiftool commandline 134 | application 135 | - Some internal improvements 136 | 137 | 2.2.1 138 | - Bugfix: Ignore filename specific tags to avoid encoding 139 | confusions. 140 | 141 | 2.2.0 142 | The Encoding Release 143 | - New option :replace_invalid_chars to handle "bad data" 144 | invalid byte sequences in UTF-8 145 | Thanks to Chris Salzberg (aka shioyama) and 146 | Robert May (aka robotmay) for precious hints 147 | - Support of different encodings for commandline params 148 | and filenames (neccessary to support Windows) 149 | to allow filenames with special chars 150 | Thanks to uwe58 and others for hints 151 | - Doing different commandline escaping for windows and POSIX 152 | systems 153 | Thanks to Michael Dungan for the hint 154 | - Update Tutorial 155 | 156 | 2.1.0 157 | - insert require 'json' 158 | - Drop option :convert_encoding (use Ruby String 159 | methods instead) 160 | - Make the test_access_coordinate work on different 161 | exiftool version. 162 | 163 | 2.0.0 164 | - Drop Ruby 1.8 compatibility. 165 | - Using JSON to parse exiftool output (need 166 | exiftool 7.65 or higher). 167 | 168 | 1.7.0 169 | - Support exiftool -c option for formatting GPS 170 | coordinates. 171 | Thanks to Lee Horrocks for the patch. 172 | - Switching from shellwords to manual escaping. 173 | Hopefully it works now on Windows systems. 174 | Thanks to uwe58 and jpg0 for the hints. 175 | 176 | 1.6.0 177 | - Type conversion in MiniExiftool.from_hash. 178 | Thanks to Ethan Soutar-Rau for the merge request. 179 | - Switching to rim. (No longer troubles with echoe.) 180 | - ExifTool version detection delayed. 181 | Thanks to Sebastian Skałacki for the merge request. 182 | - New method MiniExiftool#save! 183 | Cherry-picked commit from Wil Gieseler. 184 | 185 | 1.5.1 186 | - Make rational values work on Ruby 1.8.7. 187 | 188 | 1.5.0 189 | - Supporting exiftool command-line option -m. 190 | rubyforge request [#29587] 191 | Thanks to Michael Grove for reporting. 192 | - Supporting rational values. 193 | 194 | 1.4.4 195 | - Fix escaping of values for older versions of Shellwords. 196 | 197 | 1.4.3 198 | - Fixing rubyforge bug [#29596] (Quotes in values) 199 | Thanks to Michael Grove for reporting 200 | 201 | 1.4.2 202 | - Add .yardopts file to gem. 203 | 204 | 1.4.1 205 | - Update documentation for using yard. 206 | 207 | 1.4.0 208 | - Allow symbols for tag access with []. 209 | - Refactoring tests. 210 | 211 | 1.3.1 212 | - Remove TestEscapeFilename test and releating test photo 213 | because the latter produces errors on windows systems. 214 | - Version check in prerelease task. 215 | 216 | 1.3.0 217 | - MiniExiftool is now ready for Ruby 1.9 218 | All tests in the test suite pass. :) 219 | 220 | 1.2.2 221 | - Fixing ptore directory naming convention for darwin. 222 | Thanks to Denis Barushev for the hint. 223 | 224 | 1.2.1 225 | - Switching to echoe. 226 | - Update e-mail address. 227 | 228 | 1.2.0 229 | - Fixing time zone handling. 230 | Thanks to ccoenen for the hint. 231 | 232 | 1.1.0 233 | - Escaping filenames in shell commands 234 | Thanks to Michael Hoy for the hint and implementing a patch which was 235 | the base for this fix. 236 | 237 | 1.0.2 238 | - Fixing warings 239 | Thanks to Peter-Hinrich Krogmann for the hint. 240 | 241 | 1.0.1 242 | - Fixing bug [#22726] 243 | Making MiniExiftool::Error public. 244 | Thanks to Mathias Stjernstrom for sending a patch. 245 | 246 | 1.0.0 247 | - Be aware changing in the interface: 248 | - List tags (e.g. Keywords, SupplementalCategories) are now handled as 249 | arrays. 250 | - Tag SubjectLocation is not longer an array value but a string value! 251 | 252 | 0.7.0 253 | - Changing composite behaviour: Composite tags are now included as standard! 254 | - New method MiniExiftool.opts which returns a hash of the standard 255 | options used for MiniExiftool.new 256 | - New option :convert_encoding for MiniExiftool.new which uses the -L-option 257 | of the exiftool command-line application (see online documentation for it) 258 | Thanks to Henning Kulander for the causing of this change. 259 | 260 | 0.6.0 261 | - New methods for serialization: 262 | - MiniExiftool.from_hash 263 | - MiniExiftool.from_yaml 264 | - MiniExiftool#to_hash 265 | - MiniExiftool#to_yaml 266 | Thanks to Andrew Bennett for the initial idea of YAML-serialization 267 | - Refactoring of tests 268 | - Small documentation update 269 | 270 | 0.5.1 271 | - Warning "parenthesize argument(s) for future version" removed 272 | Thanks to Greg from knobby.ws 273 | 274 | 0.5.0 275 | - New option :timestamps to create DateTime objects instead of Time objects 276 | for timestamps (Fixing bug #16328) 277 | - Invalid values of timestamps (i.e. 0000:00:00 00:00:00) are now mapped 278 | to false 279 | 280 | 0.4.1 281 | - Compatibility for Ruby 1.9 282 | 283 | 0.4.0 284 | - MiniExiftool::Error inherits now from StandardError 285 | - Alternative installation via setup.rb 286 | - Bugfix 287 | Saving of non-read tags doesn't work with tags with hyphen 288 | Thanks to Robin Romahn for reporting the bug 289 | - New methods: MiniExiftool.all_tags and MiniExiftool.original_tag 290 | - Internal: Original tag names (all and writable) are now saved via pstore in 291 | a file for better performance 292 | 293 | 0.3.1 294 | - Typos fixed 295 | 296 | 0.3.0 297 | - Documentation completed and a Mini Tutorial added 298 | - Interface changes: 299 | - Test if a value for a tag can be saved is now done in 300 | MiniExiftool#save 301 | => There is no check at the moment you set a value: 302 | the tag occurs in MiniExiftool#changed_values 303 | => While calling MiniExiftool#save errors can occur (see next point) 304 | - MiniExiftool#save is a transaction: if one or more error occurs the file is 305 | not changed! In such a case the errors can be found in MiniExiftool#errors 306 | - Parameter opts of MiniExiftool.initialize is now a Hash with two options: 307 | - :numerical => read metadata as numerical values 308 | - :composite => read also composite tags 309 | - Tests added 310 | 311 | 0.2.0 312 | - Better error handling (i.e. error messages) 313 | - Checking if the exiftool command can be executed at loading the lib 314 | - New class method exiftool_version 315 | - Added tests 316 | - Documentation completed 317 | 318 | 0.1.2 319 | - Bugfix for Windows (Tempfile) 320 | Thanks to Jérome Soika for testing 321 | - Regexes optimized (a little bit) 322 | - New class-method MiniExiftool.writable_tags 323 | 324 | 0.1.1 325 | - Fixing bug [#8073] 326 | Handling the '-' in tag Self-timer 327 | Thanks to Eric Young 328 | 329 | 0.1.0 330 | - New method "revert" 331 | - More tests 332 | 333 | 0.0.1 334 | - Initial release 335 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiniExiftool ![Test Status](https://github.com/janfri/mini_exiftool/actions/workflows/main.yml/badge.svg) 2 | 3 | This library is a wrapper for the ExifTool command-line application 4 | (https://exiftool.org) written by Phil Harvey. 5 | It provides the full power of ExifTool to Ruby: reading and writing of 6 | EXIF-data, IPTC-data and XMP-data. 7 | 8 | ## Requirements 9 | 10 | Ruby 1.9 or higher and an installation of the ExifTool 11 | command-line application at least version 7.65. 12 | If you run on Ruby 1.8 or with a prior exiftool version 13 | install mini_exiftool version 1.x.x. 14 | Instructions for installation you can find under 15 | https://exiftool.org/install.html . 16 | 17 | Alternatively Wil Gieseler has bundled a meta-gem that eliminates the 18 | need for a separate ExifTool installation. Have a look at 19 | http://github.com/wilg/mini_exiftool_vendored or 20 | http://rubygems.org/gems/mini_exiftool_vendored . 21 | 22 | ## Installation 23 | 24 | First you need ExifTool (see under Requirements above). Then you can simply 25 | install the gem with 26 | ```sh 27 | gem install mini_exiftool 28 | ``` 29 | or simply add to the following to your Gemfile: 30 | ```ruby 31 | gem 'mini_exiftool' 32 | ``` 33 | 34 | If you need to support older versions of Ruby or Exiftool (see Requirements above) 35 | 36 | ```sh 37 | gem install --version "< 2.0.0" mini_exiftool 38 | ``` 39 | 40 | or if you use a Gemfile add: 41 | 42 | ```ruby 43 | gem 'mini_exiftool', '<2.0.0' 44 | ``` 45 | 46 | ## Configuration 47 | 48 | You can manually set the exiftool command that should be used via 49 | 50 | ```ruby 51 | MiniExiftool.command = '/path/to/my/exiftool' 52 | ``` 53 | 54 | In addition, you can also tell MiniExiftool where to store the PStore files with tags 55 | which exiftool supports. The PStore files are used for performance issues. 56 | Per default the PStore files are stored in a sub directory `mini_exiftool` in 57 | `Gem.cache' or if this is not defined under `USERPROFILE` or `Dir.tmpdir`. 58 | 59 | You can change it by setting the environment variable 60 | `MINI_EXIFTOOL_PSTORE_DIR` or programmatically, as follows. 61 | 62 | ```ruby 63 | MiniExiftool.pstore_dir = '/path/to/pstore/dir' 64 | ``` 65 | 66 | If you're using Rails, this is easily done with 67 | 68 | ```ruby 69 | MiniExiftool.pstore_dir = Rails.root.join('tmp').to_s 70 | ``` 71 | 72 | Important hint: if you have to change the configuration you have to do this direct 73 | after `require 'mini_exiftool'`. 74 | 75 | ## Usage 76 | 77 | In general MiniExiftool is very intuitive to use as the following examples show: 78 | 79 | ```ruby 80 | # Reading meta data from a file 81 | photo = MiniExiftool.new 'photo.jpg' 82 | puts photo.title 83 | 84 | # Alternative reading meta data from an IO instance 85 | photo = MiniExiftool.new io 86 | puts photo.title 87 | 88 | # Writing meta data 89 | photo = MiniExiftool.new 'photo.jpg' 90 | photo.title = 'This is the new title' 91 | photo.save 92 | 93 | # Copying meta data 94 | photo = MiniExiftool.new('photo.jpg') 95 | photo.copy_tags_from('another_photo.jpg', :author) 96 | ``` 97 | 98 | For further information about using MiniExiftool read the Tutorial.md. 99 | in the project root folder and have a look at the examples in directory 100 | examples. 101 | 102 | ## Caveats 103 | 104 | The philosophy of MiniExiftool is safety over performance. 105 | It can not handle more than one file at once. Writing operations are 106 | executed on a copy of the original file to have atomic writing: Either 107 | all changed values are written or none. 108 | To be able to assign errors to a specific tag writing operations also call 109 | the Exiftool command-line application once for each changed tag! 110 | 111 | In short: MiniExiftool has a very bad performance especially at write operations. 112 | 113 | **If you work with many files it is strongly recommended not to use MiniExiftool 114 | but instead my other gem [MultiExiftool](https://github.com/janfri/multi_exiftool). 115 | This is designed to handle many files and do reading and writing fast.** 116 | 117 | ## Encodings 118 | 119 | In MiniExiftool all strings are encoded in UTF-8. If you need other 120 | encodings in your project use the String#encod* methods. 121 | 122 | If you have problems with corrupted strings when using MiniExiftool 123 | there are two reasons for this: 124 | 125 | ### Internal character sets 126 | 127 | You can specify the charset in which the meta data is in the file encoded 128 | if you read or write to some sections of meta data (i.e. IPTC, XMP ...). 129 | It exists various options of the form *_encoding: exif, iptc, xmp, png, 130 | id3, pdf, photoshop, quicktime, aiff, mie and vorbis. 131 | 132 | For IPTC meta data it is recommended to set also the CodedCharacterSet 133 | tag. 134 | 135 | Please read the section about the character sets of the ExifTool command 136 | line application carefully to understand what's going on 137 | (https://exiftool.org/faq.html#Q10)! 138 | 139 | ```ruby 140 | # Using UTF-8 as internal encoding for IPTC tags and MacRoman 141 | # as internal encoding for EXIF tags 142 | photo = MiniExiftool.new('photo.jpg', iptc_encoding: 'UTF8', 143 | exif_encoding: 'MacRoman' 144 | # IPTC CaptionAbstract is already UTF-8 encoded 145 | puts photo.caption_abstract 146 | # EXIF Comment is converted from MacRoman to UTF-8 147 | puts photo.comment 148 | 149 | photo = MiniExiftool.new('photo.jpg', iptc_encoding: 'UTF8', 150 | exif_encoding: 'MacRoman' 151 | # When saving IPTC data setting CodedCharacterSet as recommended 152 | photo.coded_character_set = 'UTF8' 153 | # IPTC CaptionAbstract will be stored in UTF-8 encoding 154 | photo.caption_abstract = 'Some text with Ümläuts' 155 | # EXIF Comment will be stored in MacRoman encoding 156 | photo.comment = 'Comment with Ümläuts' 157 | photo.save 158 | ``` 159 | 160 | ### Corrupt characters 161 | 162 | You use the correct internal character set but in the string are still corrupt 163 | characters. 164 | This problem you can solve with the option `replace_invalid_chars`: 165 | 166 | ```ruby 167 | # Replace all invalid characters with a question mark 168 | photo = MiniExiftool.new('photo.jpg', replace_invalid_chars: '?') 169 | ``` 170 | 171 | ## Contribution 172 | 173 | The code is hosted in a git repository on github at 174 | https://github.com/janfri/mini_exiftool 175 | feel free to contribute! 176 | 177 | ## Versioning 178 | 179 | MiniExiftool follows [Semantic Versioning](https://semver.org/), both SemVer and 180 | SemVerTag. 181 | 182 | ## Author 183 | Jan Friedrich 184 | 185 | ## Copyright / License 186 | Copyright (c) 2007-2019 by Jan Friedrich 187 | 188 | Licensed under terms of the GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1, 189 | February 1999 (see file COPYING for more details) 190 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rim/tire' 2 | require 'rim/version' 3 | require 'rim/regtest' 4 | 5 | Rim.setup do |p| 6 | p.test_warning = false 7 | if p.feature_loaded? 'rim/aspell' 8 | p.aspell_files << 'Tutorial.md' 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * Tests for managing tags with pstore 2 | * Looking for a solution to dump and restore DateTime instances with YAML 3 | -------------------------------------------------------------------------------- /Tutorial.md: -------------------------------------------------------------------------------- 1 | # Mini Tutorial 2 | 3 | 4 | ## Installation 5 | 6 | * Installing the ExifTool command-line application from Phil Harvey 7 | (see https://exiftool.org/install.html) 8 | * Installing the Ruby library (`gem install mini_exiftool`) 9 | 10 | 11 | ## Lesson 1: Reading Meta Data 12 | 13 | ### A Simple Example 14 | 15 | ```ruby 16 | require 'mini_exiftool' 17 | 18 | photo = MiniExiftool.new 'photo.jpg' 19 | puts photo['DateTimeOriginal'] 20 | ``` 21 | 22 | ### Smart Tag Names 23 | In the example above we use `photo['DateTimeOriginal']` to 24 | get the value for the time the photo was taken. But tag names are not 25 | case sensitive and additional underlines are also irrelevant. So 26 | following expressions are equivalent: 27 | ```ruby 28 | photo['DateTimeOriginal'] 29 | photo['datetimeoriginal'] 30 | photo['date_time_original'] 31 | ``` 32 | 33 | It is also possible to use symbols: 34 | ```ruby 35 | photo[:DateTimeOriginal] 36 | photo[:datetimeoriginal] 37 | photo[:date_time_original] 38 | ``` 39 | 40 | ### Nicer Access Via Dynamic Methods 41 | 42 | Using the []-method is the safest way to access to values of tags 43 | (e. g. Self-timer you can only access this way) but the smarter way is 44 | using dynamic method access. You can write: 45 | ```ruby 46 | photo.datetimeoriginal 47 | ``` 48 | or also 49 | ```ruby 50 | photo.date_time_original 51 | ``` 52 | 53 | ### Value Types 54 | 55 | Following types of values are at the moment supported: 56 | * Array (e. g. Keywords => ['tree', 'gras']) 57 | * Integer (e. g. ISO => 400) 58 | * Float (e. g. FNumber => 9.5) 59 | * String (e. g. Model => DYNAX 7D) 60 | * Time (e. g. DateTimeOriginal => 2005:09:13 20:08:50) 61 | 62 | Be aware, if there is only one value in a tag which can hold multiple 63 | values the result isn't an array! But you can get one with the Array 64 | method: 65 | ```ruby 66 | # only _one_ keyword 67 | p1 = MiniExiftool.new 'p1.jpg' 68 | p1.keywords # => 'red' 69 | # _more than one_ keywords 70 | p3 = MiniExiftool.new 'p3.jpg' 71 | p3.keywords # => ['red', 'yellow', 'green'] 72 | 73 | # if we want to get an array in both cases and don't know 74 | # if there is one ore more values set let's take Array() 75 | Array(p1.keywords) # => ['red'] 76 | Array(p3.keywords) # => ['red', 'yellow', 'green'] 77 | ``` 78 | 79 | ### Using options 80 | 81 | The ExifTool command-line application has an option (-n) to get values 82 | as numbers if possible, in MiniExiftool you can do this with setting 83 | the `:numerical` option to `true` while generating a new 84 | instance with new or using the `numerical=`-method 85 | combining with calling `reload`. 86 | 87 | Let's look at an example: 88 | ```ruby 89 | # standard: numerical is false 90 | photo = MiniExiftool.new 'photo.jpg' 91 | photo.exposure_time # => '1/60 (Rational) 92 | # now with numerical is true 93 | photo.numerical = true 94 | photo.reload 95 | photo.exposure_time # => 0.01666667 (Float) 96 | ``` 97 | 98 | This behaviour can be useful if you want to do calculations on the 99 | value, if you only want to show the value the standard behaviour is 100 | maybe better. 101 | 102 | The Time class of Ruby cannot handle timestamps before 1st January 1970 103 | on some platforms. If there are timestamps in files before this date it 104 | will result in an error. In this case we can set the option 105 | `:timestamps` to `DateTime` to use DateTime objects instead 106 | of Time objects. 107 | 108 | There is another option `:composite`. If this is set to 109 | `false` the composite tags are not calculated by the exiftool 110 | command-line application (option -e). 111 | 112 | Further options are 113 | * `:ignore_minor_errors` to ignore minor 114 | errors (See -m-option of the exiftool command-line application, 115 | default is `false`) 116 | * `:coord_format` set format for GPS coordinates (See 117 | -c-option of the exiftool command-line application, default is `nil` 118 | that means exiftool standard) 119 | * `:fast` useful when reading JPEGs over a slow network connection 120 | (See -fast-option of the exiftool command-line application, default is `false`) 121 | * `:fast2` useful when reading JPEGs over a slow network connection 122 | (See -fast2-option of the exiftool command-line application, default is `false`) 123 | * `:replace_invalid_chars` replace string for invalid 124 | UTF-8 characters or `false` if no replacing should be done, 125 | default is `false` 126 | * `:exif_encoding`, `:iptc_encoding`, 127 | `:xmp_encoding`, `:png_encoding`, 128 | `:id3_encoding`, `:pdf_encoding`, 129 | `:photoshop_encoding`, `:quicktime_encoding`, 130 | `:aiff_encoding`, `:mie_encoding`, 131 | `:vorbis_encoding` to set this specific encoding (see 132 | -charset option of the exiftool command-line application, default is 133 | `nil`: no encoding specified) 134 | 135 | ### Using an IO instance 136 | 137 | ```ruby 138 | require 'mini_exiftool' 139 | require 'open3' 140 | 141 | # Using external curl command 142 | input, output = Open3.popen2("curl -s http://www.url.of.a.photo") 143 | input.close 144 | photo = MiniExiftool.new output 145 | puts photo['ISO'] 146 | ``` 147 | 148 | The kind of the parameter `filename_or_io` is determined via duck typing: 149 | if the argument responds to `to_str` it is interpreted as filename, if it 150 | responds to `read` it is interpreted es IO instance. 151 | Attention: If you use an IO instance then writing of values is not supported! 152 | 153 | Look at the show_speedup_with_fast_option example in the MiniExiftool examples 154 | directory for more details about using an IO instance. 155 | 156 | 157 | ## Lesson 2: Writing Meta Data 158 | 159 | ### Also A Very Simple Example 160 | 161 | ```ruby 162 | require 'mini_exiftool' 163 | 164 | photo = MiniExiftool.new 'photo.jpg' 165 | photo.comment = 'hello world' 166 | photo.save 167 | ``` 168 | 169 | 170 | ### Save Is Atomar 171 | 172 | If you have changed several values and call the `save`-method either 173 | all changes will be written to the file or nothing. The return value 174 | of the `save`-method is `true` if all values are written to the file 175 | otherwise save returns `false`. In the last case you can use the 176 | `errors`-method which returns a hash of the tags which values couldn't 177 | be written with an error message for each of them. 178 | 179 | 180 | ### Interesting Methods 181 | 182 | Have a look at the `changed?`-method for checking if the 183 | value of a specific tag is changed or a changing in general is 184 | done. In the same way the `revert`-method reverts the value of a 185 | specific tag or in general all changes. 186 | 187 | You should also look at the rdoc information of MiniExiftool. 188 | 189 | 190 | ## Lesson 3: Copying Meta Data 191 | 192 | ### Examples 193 | 194 | ```ruby 195 | require 'mini_exiftool' 196 | 197 | photo = MiniExiftool.new('photo.jpg') 198 | 199 | # Update the author tag of photo.jpg with the value of the author tag 200 | # of another_photo.jpg 201 | photo.copy_tags_from('another_photo.jpg', 'Author') 202 | 203 | # It's also possible to use symbols and case is also not meaningful 204 | photo.copy_tags_from('another_photo.jpg', :author) 205 | 206 | # Further more than one tag can be copied at once 207 | photo.copy_tags_from('another_photo', %w[author copyright]) 208 | ``` 209 | 210 | Look at the file copy_icc_profile.rb in the examples folder of MiniExiftool. 211 | 212 | 213 | ## Further Examples 214 | 215 | Have a look in the examples folder of MiniExiftool. 216 | -------------------------------------------------------------------------------- /examples/copy_icc_profile.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'mini_exiftool' 3 | 4 | if ARGV.size < 2 5 | puts "usage: ruby #{__FILE__} SOURCE_FILE TARGET_FILE" 6 | exit -1 7 | end 8 | 9 | source_filename, target_filename = ARGV 10 | 11 | begin 12 | photo = MiniExiftool.new filename 13 | # The second parameter of MiniExiftool#copy_tags_from 14 | # could be a String, Symbol or an Array of Strings, 15 | # Symbols 16 | photo.copy_tags_from(target, 'icc_profile') 17 | rescue MiniExiftool::Error => e 18 | $stderr.puts e.message 19 | exit -1 20 | end 21 | -------------------------------------------------------------------------------- /examples/external_photo.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'open-uri' 3 | require 'rubygems' 4 | require 'mini_exiftool' 5 | 6 | unless ARGV.size == 1 7 | puts "usage: ruby #{__FILE__} URI" 8 | puts " i.e.: ruby #{__FILE__} http://www.23hq.com/janfri/photo/1535332/large" 9 | exit -1 10 | end 11 | 12 | # Fetch an external photo 13 | filename = open(ARGV.first).path 14 | 15 | # Read the metadata 16 | photo = MiniExiftool.new filename 17 | 18 | # Print the metadata 19 | photo.tags.sort.each do |tag| 20 | # puts "#{tag}: #{photo[tag]}" 21 | puts tag.ljust(28) + photo[tag].to_s 22 | end 23 | -------------------------------------------------------------------------------- /examples/print_portraits.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'rubygems' 3 | require 'mini_exiftool' 4 | 5 | unless ARGV.size > 0 6 | puts "usage: ruby #{__FILE__} FILES" 7 | puts " i.e.: ruby #{__FILE__} *.jpg" 8 | exit -1 9 | end 10 | 11 | # Loop at all given files 12 | ARGV.each do |filename| 13 | # If a given file isn't a photo MiniExiftool new method will throw 14 | # an exception this we will catch 15 | begin 16 | photo = MiniExiftool.new filename 17 | height = photo.image_height 18 | width = photo.image_width 19 | # We define portait as a photo wich ratio of height to width is 20 | # larger than 0.7 21 | if height / width > 0.7 22 | puts filename 23 | end 24 | rescue MiniExiftool::Error => e 25 | $stderr.puts e.message 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /examples/shift_time.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'rubygems' 3 | require 'mini_exiftool' 4 | 5 | if ARGV.size < 2 6 | puts "usage: ruby #{__FILE__} [+|-]SECONDS FILES" 7 | puts " i.e.: ruby #{__FILE__} 3600 *.jpg" 8 | exit -1 9 | end 10 | 11 | delta = ARGV.shift.to_i 12 | 13 | ARGV.each do |filename| 14 | begin 15 | photo = MiniExiftool.new filename 16 | rescue MiniExiftool::Error => e 17 | $stderr.puts e.message 18 | exit -1 19 | end 20 | time = photo.date_time_original 21 | # time is a Time object, so we can use the methods of it :) 22 | photo.date_time_original = time + delta 23 | save_ok = photo.save 24 | if save_ok 25 | fmt = '%Y-%m-%d %H:%M:%S' 26 | puts "#{filename} changed: #{time.strftime(fmt)} -> #{(time + delta).strftime(fmt)}" 27 | else 28 | puts "#{filename} could not be changed" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /examples/show_speedup_with_fast_option.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'mini_exiftool' 3 | require 'open3' 4 | 5 | unless ARGV.size == 1 6 | puts "usage: ruby #{__FILE__} URI" 7 | puts " i.e.: ruby #{__FILE__} http://farm6.staticflickr.com/5015/5458914734_8fd3f33278_o.jpg" 8 | exit -1 9 | end 10 | 11 | arg = ARGV.shift 12 | 13 | #################################### 14 | # Helper methods 15 | #################################### 16 | 17 | def time 18 | a = Time.now 19 | yield 20 | b = Time.now 21 | b - a 22 | end 23 | 24 | def print_statistics name, without_fast, fast, fast2 25 | puts '-' * 40 26 | puts name, "\n" 27 | puts format 'without fast: %0.2fs', without_fast 28 | puts format 'fast : %0.2fs', fast 29 | puts format 'fast2 : %0.2fs', fast2 30 | puts 31 | puts format 'speedup fast : %0.2f', without_fast / fast 32 | puts format 'speedup fast2: %0.2f', without_fast / fast2 33 | puts '-' * 40 34 | puts 35 | end 36 | 37 | #################################### 38 | # Plain Ruby with standard library 39 | #################################### 40 | 41 | require 'net/http' 42 | 43 | uri = URI(arg) 44 | 45 | def read_from_http uri, io 46 | Thread.new(uri, io) do |uri, io| 47 | Net::HTTP.start(uri.host, uri.port) do |http| 48 | request = Net::HTTP::Get.new uri 49 | http.request request do |response| 50 | response.read_body do |chunk| 51 | io.write chunk 52 | end 53 | end 54 | end 55 | io.close 56 | end 57 | end 58 | 59 | without_fast = time do 60 | output, input = IO.pipe 61 | read_from_http uri, input 62 | MiniExiftool.new output 63 | end 64 | 65 | fast = time do 66 | output, input = IO.pipe 67 | read_from_http uri, input 68 | MiniExiftool.new output, fast: true 69 | end 70 | 71 | fast2 = time do 72 | output, input = IO.pipe 73 | read_from_http uri, input 74 | MiniExiftool.new output, fast2: true 75 | end 76 | 77 | print_statistics 'net/http', without_fast, fast, fast2 78 | 79 | #################################### 80 | # curl 81 | #################################### 82 | 83 | without_fast = time do 84 | input, output = Open3.popen3("curl -s #{arg}") 85 | input.close 86 | MiniExiftool.new output 87 | end 88 | 89 | fast = time do 90 | input, output = Open3.popen3("curl -s #{arg}") 91 | input.close 92 | MiniExiftool.new output, fast: true 93 | end 94 | 95 | fast2 = time do 96 | input, output = Open3.popen3("curl -s #{arg}") 97 | input.close 98 | MiniExiftool.new output, fast2: true 99 | end 100 | 101 | print_statistics 'curl', without_fast, fast, fast2 102 | 103 | #################################### 104 | # wget 105 | #################################### 106 | 107 | without_fast = time do 108 | input, output = Open3.popen3("wget -q -O - #{arg}") 109 | input.close 110 | MiniExiftool.new output 111 | end 112 | 113 | fast = time do 114 | input, output = Open3.popen3("wget -q -O - #{arg}") 115 | input.close 116 | MiniExiftool.new output, fast: true 117 | end 118 | 119 | fast2 = time do 120 | input, output = Open3.popen3("wget -q -O - #{arg}") 121 | input.close 122 | MiniExiftool.new output, fast2: true 123 | end 124 | 125 | print_statistics 'wget', without_fast, fast, fast2 126 | 127 | -------------------------------------------------------------------------------- /lib/mini_exiftool.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: false 3 | # 4 | # MiniExiftool 5 | # 6 | # This library is a wrapper for the ExifTool command-line 7 | # application (https://exiftool.org/) 8 | # written by Phil Harvey. 9 | # Read and write access is done in a clean OO manner. 10 | # 11 | # Author: Jan Friedrich 12 | # Copyright (c) 2007-2021, 2023-2024 by Jan Friedrich 13 | # Licensed under the GNU LESSER GENERAL PUBLIC LICENSE, 14 | # Version 2.1, February 1999 15 | # 16 | 17 | require 'fileutils' 18 | require 'json' 19 | require 'open3' 20 | require 'pathname' 21 | require 'pstore' 22 | require 'rational' 23 | require 'rbconfig' 24 | require 'set' 25 | require 'tempfile' 26 | require 'time' 27 | require 'yaml' 28 | 29 | # Simple OO access to the ExifTool command-line application. 30 | class MiniExiftool 31 | 32 | VERSION = '2.14.0' 33 | 34 | # Name of the ExifTool command-line application 35 | @@cmd = 'exiftool' 36 | 37 | # Hash of the standard options used when call MiniExiftool.new 38 | @@opts = { :numerical => false, :composite => true, :fast => false, :fast2 => false, 39 | :ignore_minor_errors => false, :replace_invalid_chars => false, 40 | :timestamps => Time } 41 | 42 | # Encoding of the filesystem (filenames in command line) 43 | @@fs_enc = Encoding.find('filesystem') 44 | 45 | def self.opts_accessor *attrs 46 | attrs.each do |a| 47 | define_method a do 48 | @opts[a] 49 | end 50 | define_method "#{a}=" do |val| 51 | @opts[a] = val 52 | end 53 | end 54 | end 55 | 56 | attr_reader :filename, :errors, :io 57 | 58 | opts_accessor :numerical, :composite, :ignore_minor_errors, 59 | :replace_invalid_chars, :timestamps 60 | 61 | @@encoding_types = %w(exif iptc xmp png id3 pdf photoshop quicktime aiff mie vorbis) 62 | 63 | def self.encoding_opt enc_type 64 | (enc_type.to_s + '_encoding').to_sym 65 | end 66 | 67 | @@encoding_types.each do |enc_type| 68 | opts_accessor encoding_opt(enc_type) 69 | end 70 | 71 | # +filename_or_io+ The kind of the parameter is determined via duck typing: 72 | # if the argument responds to +to_str+ it is interpreted as filename, if it 73 | # responds to +read+ it is interpreted es IO instance. 74 | # 75 | # ATTENTION: If using an IO instance writing of meta data is not supported! 76 | # 77 | # +opts+ support at the moment 78 | # * :numerical for numerical values, default is +false+ 79 | # * :composite for including composite tags while loading, 80 | # default is +true+ 81 | # * :ignore_minor_errors ignore minor errors (See -m-option 82 | # of the exiftool command-line application, default is +false+) 83 | # * :coord_format set format for GPS coordinates (See 84 | # -c-option of the exiftool command-line application, default is +nil+ 85 | # that means exiftool standard) 86 | # * :fast useful when reading JPEGs over a slow network connection 87 | # (See -fast-option of the exiftool command-line application, default is +false+) 88 | # * :fast2 useful when reading JPEGs over a slow network connection 89 | # (See -fast2-option of the exiftool command-line application, default is +false+) 90 | # * :replace_invalid_chars replace string for invalid 91 | # UTF-8 characters or +false+ if no replacing should be done, 92 | # default is +false+ 93 | # * :timestamps generating DateTime objects instead of 94 | # Time objects if set to DateTime, default is +Time+ 95 | # 96 | # ATTENTION: Time objects are created using Time.local 97 | # therefore they use your local timezone, DateTime objects instead 98 | # are created without timezone! 99 | # * :exif_encoding, :iptc_encoding, 100 | # :xmp_encoding, :png_encoding, 101 | # :id3_encoding, :pdf_encoding, 102 | # :photoshop_encoding, :quicktime_encoding, 103 | # :aiff_encoding, :mie_encoding, 104 | # :vorbis_encoding to set this specific encoding (see 105 | # -charset option of the exiftool command-line application, default is 106 | # +nil+: no encoding specified) 107 | def initialize filename_or_io=nil, opts={} 108 | @opts = @@opts.merge opts 109 | if @opts[:convert_encoding] 110 | warn 'Option :convert_encoding is not longer supported!' 111 | warn 'Please use the String#encod* methods.' 112 | end 113 | @filename = nil 114 | @io = nil 115 | @values = TagHash.new 116 | @changed_values = TagHash.new 117 | @errors = TagHash.new 118 | load filename_or_io unless filename_or_io.nil? 119 | end 120 | 121 | def initialize_from_hash hash # :nodoc: 122 | set_values hash 123 | set_opts_by_heuristic 124 | self 125 | end 126 | 127 | def initialize_from_json json # :nodoc: 128 | @output = json 129 | @errors.clear 130 | parse_output 131 | self 132 | end 133 | 134 | # Load the tags of filename or io. 135 | def load filename_or_io 136 | if filename_or_io.respond_to?(:to_str) || filename_or_io.kind_of?(Pathname) # String-like 137 | unless filename_or_io && File.exist?(filename_or_io) 138 | raise MiniExiftool::Error.new("File '#{filename_or_io}' does not exist.") 139 | end 140 | if File.directory?(filename_or_io) 141 | raise MiniExiftool::Error.new("'#{filename_or_io}' is a directory.") 142 | end 143 | @filename = filename_or_io.to_s 144 | elsif filename_or_io.respond_to? :read # IO-like 145 | @io = filename_or_io 146 | @filename = '-' 147 | else 148 | raise MiniExiftool::Error.new("Could not open filename_or_io.") 149 | end 150 | @values.clear 151 | @changed_values.clear 152 | params = '-j ' 153 | params << (@opts[:numerical] ? '-n ' : '') 154 | params << (@opts[:composite] ? '' : '-e ') 155 | params << (@opts[:coord_format] ? "-c #{escape(@opts[:coord_format])}" : '') 156 | params << (@opts[:fast] ? '-fast ' : '') 157 | params << (@opts[:fast2] ? '-fast2 ' : '') 158 | params << generate_encoding_params 159 | if run(cmd_gen(params, @filename)) 160 | parse_output 161 | else 162 | raise MiniExiftool::Error.new(@error_text) 163 | end 164 | self 165 | end 166 | 167 | # Reload the tags of an already read file. 168 | def reload 169 | load @filename 170 | end 171 | 172 | # Returns the value of a tag. 173 | def [] tag 174 | @changed_values[tag] || @values[tag] 175 | end 176 | 177 | # Set the value of a tag. 178 | def []= tag, val 179 | @changed_values[tag] = val 180 | end 181 | 182 | # Returns true if any tag value is changed or if the value of a 183 | # given tag is changed. 184 | def changed? tag=false 185 | if tag 186 | @changed_values.include? tag 187 | else 188 | !@changed_values.empty? 189 | end 190 | end 191 | 192 | # Revert all changes or the change of a given tag. 193 | def revert tag=nil 194 | if tag 195 | val = @changed_values.delete(tag) 196 | res = val != nil 197 | else 198 | res = @changed_values.size > 0 199 | @changed_values.clear 200 | end 201 | res 202 | end 203 | 204 | # Returns an array of the tags (original tag names) of the read file. 205 | def tags 206 | @values.keys.map { |key| MiniExiftool.original_tag(key) } 207 | end 208 | 209 | # Returns an array of all changed tags. 210 | def changed_tags 211 | @changed_values.keys.map { |key| MiniExiftool.original_tag(key) } 212 | end 213 | 214 | # Save the changes to the file. 215 | def save 216 | if @io 217 | raise MiniExiftool::Error.new('No writing support when using an IO.') 218 | end 219 | return false if @changed_values.empty? 220 | @errors.clear 221 | temp_file = Tempfile.new('mini_exiftool') 222 | temp_file.close 223 | temp_filename = temp_file.path 224 | FileUtils.cp filename.encode(@@fs_enc), temp_filename 225 | all_ok = true 226 | @changed_values.each do |tag, val| 227 | original_tag = MiniExiftool.original_tag(tag) 228 | arr_val = val.kind_of?(Array) ? val : [val] 229 | arr_val.map! {|e| convert_before_save(e)} 230 | params = '-q -P -overwrite_original ' 231 | params << (arr_val.detect {|x| x.kind_of?(Numeric)} ? '-n ' : '') 232 | params << (@opts[:ignore_minor_errors] ? '-m ' : '') 233 | params << generate_encoding_params 234 | arr_val.each do |v| 235 | params << %Q(-#{original_tag}=#{escape(v)} ) 236 | end 237 | result = run(cmd_gen(params, temp_filename)) 238 | unless result 239 | all_ok = false 240 | @errors[tag] = @error_text.gsub(/Nothing to do.\n\z/, '').chomp 241 | end 242 | end 243 | if all_ok 244 | FileUtils.cp temp_filename, filename.encode(@@fs_enc) 245 | reload 246 | end 247 | temp_file.delete 248 | all_ok 249 | end 250 | 251 | def save! 252 | unless save 253 | err = [] 254 | @errors.each do |key, value| 255 | err << "(#{key}) #{value}" 256 | end 257 | raise MiniExiftool::Error.new("MiniExiftool couldn't save. The following errors occurred: #{err.empty? ? "None" : err.join(", ")}") 258 | end 259 | end 260 | 261 | def copy_tags_from(source_filename, tags) 262 | @errors.clear 263 | unless File.exist?(source_filename) 264 | raise MiniExiftool::Error.new("Source file #{source_filename} does not exist!") 265 | end 266 | params = '-q -P -overwrite_original ' 267 | tags_params = Array(tags).map {|t| '-' << t.to_s}.join(' ') 268 | cmd = [@@cmd, params, '-tagsFromFile', escape(source_filename).encode(@@fs_enc), tags_params.encode('UTF-8'), escape(filename).encode(@@fs_enc)].join(' ') 269 | cmd.force_encoding('UTF-8') 270 | result = run(cmd) 271 | reload 272 | result 273 | end 274 | 275 | # Returns a hash of the original loaded values of the MiniExiftool 276 | # instance. 277 | def to_hash 278 | result = {} 279 | @values.each do |k,v| 280 | result[MiniExiftool.original_tag(k)] = v 281 | end 282 | result 283 | end 284 | 285 | # Returns a YAML representation of the original loaded values of the 286 | # MiniExiftool instance. 287 | def to_yaml 288 | to_hash.to_yaml 289 | end 290 | 291 | # Create a MiniExiftool instance from a hash. Default value 292 | # conversions will be applied if neccesary. 293 | def self.from_hash hash, opts={} 294 | instance = MiniExiftool.new nil, opts 295 | instance.initialize_from_hash hash 296 | instance 297 | end 298 | 299 | # Create a MiniExiftool instance from JSON data. Default value 300 | # conversions will be applied if neccesary. 301 | def self.from_json json, opts={} 302 | instance = MiniExiftool.new nil, opts 303 | instance.initialize_from_json json 304 | instance 305 | end 306 | 307 | # Create a MiniExiftool instance from YAML data created with 308 | # MiniExiftool#to_yaml 309 | def self.from_yaml yaml, opts={} 310 | MiniExiftool.from_hash YAML.unsafe_load(yaml), opts 311 | end 312 | 313 | # Returns the command name of the called ExifTool application. 314 | def self.command 315 | @@cmd 316 | end 317 | 318 | # Setting the command name of the called ExifTool application. 319 | def self.command= cmd 320 | @@cmd = cmd 321 | end 322 | 323 | # Returns the options hash. 324 | def self.opts 325 | @@opts 326 | end 327 | 328 | # Returns a set of all known tags of ExifTool. 329 | def self.all_tags 330 | unless defined? @@all_tags 331 | @@all_tags = pstore_get :all_tags 332 | end 333 | @@all_tags 334 | end 335 | 336 | # Returns a set of all possible writable tags of ExifTool. 337 | def self.writable_tags 338 | unless defined? @@writable_tags 339 | @@writable_tags = pstore_get :writable_tags 340 | end 341 | @@writable_tags 342 | end 343 | 344 | # Returns the original ExifTool name of the given tag 345 | def self.original_tag tag 346 | unless defined? @@all_tags_map 347 | @@all_tags_map = pstore_get :all_tags_map 348 | end 349 | @@all_tags_map[tag] 350 | end 351 | 352 | # Returns the version of the ExifTool command-line application. 353 | def self.exiftool_version 354 | Open3.popen3 "#{MiniExiftool.command} -ver" do |_inp, out, _err, _thr| 355 | out.read.chomp! 356 | end 357 | rescue SystemCallError 358 | raise MiniExiftool::Error.new("Command '#{MiniExiftool.command}' not found") 359 | end 360 | 361 | def self.unify tag 362 | tag.to_s.gsub(/[-_]/,'').downcase 363 | end 364 | 365 | @@running_on_windows = /mswin|mingw|cygwin/ === RbConfig::CONFIG['host_os'] 366 | 367 | def self.pstore_dir 368 | unless defined? @@pstore_dir 369 | @@pstore_dir = 370 | if env = ENV['MINI_EXIFTOOL_PSTORE_DIR'] 371 | env 372 | elsif defined?(Gem.cache_home) && File.writable?(Gem.cache_home) 373 | File.join(Gem.cache_home, 'mini_exiftool') 374 | else 375 | # This fallback will hopefully work on *NIX and Windows systems 376 | cache_dir = ENV['USERPROFILE'] || Dir.tmpdir 377 | File.join(cache_dir, 'mini_exiftool') 378 | end 379 | end 380 | @@pstore_dir 381 | end 382 | 383 | def self.pstore_dir= dir 384 | @@pstore_dir = dir 385 | end 386 | 387 | # Exception class 388 | class MiniExiftool::Error < StandardError; end 389 | 390 | ############################################################################ 391 | private 392 | ############################################################################ 393 | 394 | def cmd_gen arg_str='', filename 395 | [+@@cmd, arg_str.encode('UTF-8'), escape(filename.encode(@@fs_enc))].map {|s| s.force_encoding('UTF-8')}.join(' ') 396 | end 397 | 398 | def run cmd 399 | if $DEBUG 400 | $stderr.puts cmd 401 | end 402 | status = Open3.popen3(cmd) do |inp, out, err, thr| 403 | if @io 404 | begin 405 | IO.copy_stream @io, inp 406 | rescue Errno::EPIPE 407 | # Output closed, no problem 408 | rescue ::IOError => e 409 | raise MiniExiftool::Error.new("IO is not readable.") 410 | end 411 | inp.close 412 | end 413 | @output = out.read 414 | @error_text = err.read 415 | thr.value.exitstatus 416 | end 417 | status == 0 418 | end 419 | 420 | def convert_before_save val 421 | case val 422 | when Time 423 | val = val.strftime('%Y:%m:%d %H:%M:%S') 424 | end 425 | val 426 | end 427 | 428 | def method_missing symbol, *args 429 | tag_name = symbol.id2name 430 | if tag_name =~ /^(.+)=$/ 431 | self[$1] = args.first 432 | else 433 | self[tag_name] 434 | end 435 | end 436 | 437 | def respond_to_missing? symbol, *args 438 | tag_name = MiniExiftool.unify(symbol.id2name) 439 | !!(tag_name =~ /=$/) || @values.key?(tag_name) || super 440 | end 441 | 442 | def parse_output 443 | adapt_encoding 444 | set_values JSON.parse(@output).first 445 | end 446 | 447 | def adapt_encoding 448 | @output.force_encoding('UTF-8') 449 | if @opts[:replace_invalid_chars] && !@output.valid_encoding? 450 | @output.encode!('UTF-16le', invalid: :replace, replace: @opts[:replace_invalid_chars]).encode!('UTF-8') 451 | end 452 | end 453 | 454 | def convert_after_load tag, value 455 | return value unless value.kind_of?(String) 456 | return value unless value.valid_encoding? 457 | case value 458 | when /^\d{4}:\d\d:\d\d \d\d:\d\d:\d\d/ 459 | s = value.sub(/^(\d+):(\d+):/, '\1-\2-') 460 | begin 461 | if @opts[:timestamps] == Time 462 | value = Time.parse(s) 463 | elsif @opts[:timestamps] == DateTime 464 | value = DateTime.parse(s) 465 | else 466 | raise MiniExiftool::Error.new("Value #{@opts[:timestamps]} not allowed for option timestamps.") 467 | end 468 | rescue ArgumentError, RangeError 469 | value = false 470 | end 471 | when /^\+\d+\.\d+$/ 472 | value = value.to_f 473 | when /^0\d+$/ 474 | # no conversion => String 475 | when /^-?\d+$/ 476 | value = value.to_i 477 | when %r(^(\d+)/(\d+)$) 478 | value = Rational($1.to_i, $2.to_i) rescue value 479 | when /^[\d ]+$/ 480 | # nothing => String 481 | end 482 | value 483 | end 484 | 485 | def set_values hash 486 | hash.each_pair do |tag,val| 487 | @values[tag] = convert_after_load(tag, val) 488 | end 489 | # Remove filename specific tags use attr_reader 490 | # MiniExiftool#filename instead 491 | # Cause: value of tag filename and attribute 492 | # filename have different content, the latter 493 | # holds the filename with full path (like the 494 | # sourcefile tag) and the former the basename 495 | # of the filename also there is no official 496 | # "original tag name" for sourcefile 497 | %w(directory filename sourcefile).each do |t| 498 | @values.delete(t) 499 | end 500 | end 501 | 502 | def set_opts_by_heuristic 503 | @opts[:composite] = tags.include?('ImageSize') 504 | @opts[:numerical] = self.file_size.kind_of?(Integer) 505 | @opts[:timestamps] = self.FileModifyDate.kind_of?(DateTime) ? DateTime : Time 506 | end 507 | 508 | def self.pstore_get attribute 509 | load_or_create_pstore unless defined? @@pstore 510 | result = nil 511 | @@pstore.transaction(true) do |ps| 512 | result = ps[attribute] 513 | end 514 | result 515 | end 516 | 517 | def self.load_or_create_pstore 518 | FileUtils.mkdir_p(pstore_dir) 519 | pstore_filename = File.join(pstore_dir, 'exiftool_tags_' << exiftool_version.gsub('.', '_') << '.pstore') 520 | @@pstore = PStore.new(pstore_filename, _threadsafe = true) 521 | if !File.exist?(pstore_filename) || File.size(pstore_filename) == 0 522 | $stderr.puts 'Generating cache file for ExifTool tag names. This takes a few seconds but is only needed once...' 523 | @@pstore.transaction do |ps| 524 | ps[:all_tags] = all_tags = determine_tags('list') 525 | ps[:writable_tags] = determine_tags('listw') 526 | map = {} 527 | all_tags.each { |k| map[unify(k)] = k } 528 | ps[:all_tags_map] = map 529 | end 530 | $stderr.puts 'Cache file generated.' 531 | end 532 | end 533 | 534 | def self.determine_tags arg 535 | output = `#{@@cmd} -#{arg}` 536 | lines = output.split(/\n/) 537 | tags = Set.new 538 | lines.each do |line| 539 | next unless line =~ /^\s/ 540 | tags |= line.chomp.split 541 | end 542 | tags 543 | end 544 | 545 | if @@running_on_windows 546 | def escape val 547 | '"' << val.to_s.gsub(/([\\"`])/, "\\\\\\1") << '"' 548 | end 549 | else 550 | def escape val 551 | '"' << val.to_s.gsub(/([\\"$`])/, "\\\\\\1") << '"' 552 | end 553 | end 554 | 555 | def generate_encoding_params 556 | params = '' 557 | @@encoding_types.each do |enc_type| 558 | if enc_val = @opts[MiniExiftool.encoding_opt(enc_type)] 559 | params << "-charset #{enc_type}=#{enc_val} " 560 | end 561 | end 562 | params 563 | end 564 | 565 | # Backport YAML.unsafe_load 566 | unless defined? YAML.unsafe_load 567 | module BackportYAML 568 | def unsafe_load *args 569 | load *args 570 | end 571 | end 572 | YAML.extend BackportYAML 573 | end 574 | 575 | # Hash with indifferent access: 576 | # DateTimeOriginal == datetimeoriginal == date_time_original 577 | class TagHash < Hash # :nodoc: 578 | def[] k 579 | super(unify(k)) 580 | end 581 | def []= k, v 582 | super(unify(k), v) 583 | end 584 | def delete k 585 | super(unify(k)) 586 | end 587 | 588 | def unify tag 589 | MiniExiftool.unify tag 590 | end 591 | end 592 | 593 | end 594 | -------------------------------------------------------------------------------- /mini_exiftool.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require_relative 'lib/mini_exiftool' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'mini_exiftool' 6 | s.version = MiniExiftool::VERSION 7 | 8 | s.author = 'Jan Friedrich' 9 | s.email = 'janfri26@gmail.com' 10 | 11 | s.license = 'LGPL-2.1' 12 | 13 | s.summary = 'This library is a wrapper for the ExifTool command-line application (https://exiftool.org).' 14 | s.description = <<~END 15 | This library is a wrapper for the ExifTool command-line application\n(https://exiftool.org) written by Phil Harvey. 16 | It provides the full power of ExifTool to Ruby: reading and writing of\nEXIF-data, IPTC-data and XMP-data. 17 | END 18 | s.homepage = 'https://github.com/janfri/mini_exiftool' 19 | 20 | s.metadata = { 21 | 'source_code_uri' => s.homepage 22 | } 23 | 24 | s.require_paths = 'lib' 25 | s.files = %w[COPYING Changelog README.md Tutorial.md] + Dir['examples/**/*'] + Dir['lib/*.rb'] 26 | 27 | s.post_install_message = <<~END 28 | +-----------------------------------------------------------------------+ 29 | | Please ensure you have installed exiftool at least version 7.65 | 30 | | and it's found in your PATH (Try 'exiftool -ver' on your commandline).| 31 | | For more details see | 32 | | https://exiftool.org/install.html | 33 | | You need also Ruby 2.3 or higher. | 34 | | If you need support for Ruby < 2.3 or exiftool < 7.65 then install | 35 | | mini_exiftool version < 2.0.0. | 36 | +-----------------------------------------------------------------------+ 37 | END 38 | 39 | s.required_ruby_version = '>= 2.3' 40 | s.requirements << 'exiftool, version >= 7,65' 41 | 42 | s.add_runtime_dependency('ostruct', '>= 0.6.0') 43 | s.add_runtime_dependency('pstore', '>= 0.1.3') 44 | 45 | s.add_development_dependency('rake', '>= 0') 46 | s.add_development_dependency('rim', '~> 3.0') 47 | s.add_development_dependency('test-unit', '>= 0') 48 | s.add_development_dependency('regtest', '~> 2') 49 | end 50 | -------------------------------------------------------------------------------- /regtest/read_all.rb: -------------------------------------------------------------------------------- 1 | require 'mini_exiftool' 2 | require 'regtest' 3 | 4 | Dir['test/data/*.jpg'].sort.each do |fn| 5 | Regtest.sample 'read ' << File.basename(fn) do 6 | h = MiniExiftool.new(fn).to_hash 7 | %w(FileModifyDate FileAccessDate FileInodeChangeDate FilePermissions).each do |tag| 8 | h.delete(tag) 9 | end 10 | h 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /regtest/read_all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sample: read Bad_PreviewIFD.jpg 3 | result: 4 | ExifToolVersion: 13.03 5 | FileSize: 8.6 kB 6 | FileType: JPEG 7 | FileTypeExtension: jpg 8 | MIMEType: image/jpeg 9 | JFIFVersion: 1.01 10 | ExifByteOrder: Big-endian (Motorola, MM) 11 | Make: NIKON CORPORATION 12 | Model: NIKON D60 13 | XResolution: 300 14 | YResolution: 300 15 | ResolutionUnit: inches 16 | Software: GIMP 2.6.11 17 | ModifyDate: 2012-05-31 21:33:12.000000000 +02:00 18 | YCbCrPositioning: Centered 19 | ExposureTime: !ruby/object:Rational 20 | denominator: 125 21 | numerator: 1 22 | FNumber: 5.6 23 | ExposureProgram: Not Defined 24 | ISO: 110 25 | ExifVersion: '0221' 26 | DateTimeOriginal: 2008-12-12 11:12:30.000000000 +01:00 27 | CreateDate: 2008-12-12 11:12:30.000000000 +01:00 28 | ComponentsConfiguration: Y, Cb, Cr, - 29 | ExposureCompensation: 0 30 | MaxApertureValue: 4.0 31 | MeteringMode: Multi-segment 32 | Flash: No Flash 33 | FocalLength: 55.0 mm 34 | MakerNoteVersion: 2.1 35 | ColorMode: Color 36 | Quality: Fine 37 | WhiteBalance: Auto 38 | Sharpness: Auto 39 | FocusMode: AF-A 40 | WhiteBalanceFineTune: 0 0 41 | WB_RBLevels: 56833 21761 1 1 42 | ProgramShift: 0 43 | ExposureDifference: 0 44 | Warning: "[minor] Bad PreviewIFD directory" 45 | ISOSetting: 110 46 | ImageBoundary: 0 0 3872 2592 47 | CropHiSpeed: Off (3904x2616 cropped to 3904x2616 at pixel 0,0) 48 | SerialNumber: 3365614 49 | VRInfoVersion: '0100' 50 | VibrationReduction: 'On' 51 | VRMode: Normal 52 | ActiveD-Lighting: 'Off' 53 | TimeZone: "-05:00" 54 | DaylightSavings: 'No' 55 | DateDisplayFormat: Y/M/D 56 | ISOExpansion: 'Off' 57 | ISO2: 112 58 | ISOExpansion2: 'Off' 59 | ToneComp: Auto 60 | LensType: G VR 61 | Lens: 55-200mm f/4-5.6 62 | FlashMode: Did Not Fire 63 | AFAreaMode: Dynamic Area (closest subject) 64 | AFPoint: Center 65 | AFPointsInFocus: Center 66 | ShootingMode: Single-Frame 67 | ColorHue: Mode3a 68 | LightSource: Natural 69 | ShotInfoVersion: '0211' 70 | HueAdjustment: 0 71 | NoiseReduction: 'Off' 72 | WB_RGGBLevels: 56833 1 1 7 73 | LensDataVersion: '0202' 74 | ExitPupilPosition: 70.6 mm 75 | AFAperture: 4.1 76 | FocusPosition: '0x22' 77 | FocusDistance: 6.31 m 78 | LensIDNumber: 144 79 | LensFStops: 4.92 80 | MinFocalLength: 55.0 mm 81 | MaxFocalLength: 201.6 mm 82 | MaxApertureAtMinFocal: 4.0 83 | MaxApertureAtMaxFocal: 5.7 84 | MCUVersion: 146 85 | EffectiveMaxAperture: 4.0 86 | SensorPixelSize: 6.05 x 6.05 um 87 | DateStampMode: 'Off' 88 | RetouchHistory: None 89 | ImageDataSize: 4725881 90 | ShutterCount: 290 91 | ImageOptimization: '' 92 | Saturation: Auto 93 | VariProgram: Auto 94 | MultiExposureVersion: '0100' 95 | MultiExposureMode: 'Off' 96 | MultiExposureShots: 0 97 | MultiExposureAutoGain: 'Off' 98 | HighISONoiseReduction: 'Off' 99 | PowerUpTime: 2008-12-12 11:11:24.000000000 +01:00 100 | FileInfoVersion: '0100' 101 | MemoryCardNumber: 0 102 | DirectoryNumber: 100 103 | FileNumber: '0081' 104 | RetouchInfoVersion: '0100' 105 | NEFBitDepth: Unknown (0 0 16723 17225) 106 | UserComment: '' 107 | SubSecTime: '00' 108 | SubSecTimeOriginal: '00' 109 | SubSecTimeDigitized: '00' 110 | FlashpixVersion: '0100' 111 | ColorSpace: sRGB 112 | ExifImageWidth: 1 113 | ExifImageHeight: 1 114 | SensingMethod: One-chip color area 115 | FileSource: Digital Camera 116 | SceneType: Directly photographed 117 | CFAPattern: "[Green,Blue][Red,Green]" 118 | XPTitle: '' 119 | XPKeywords: '' 120 | Compression: JPEG (old-style) 121 | ThumbnailOffset: 3690 122 | ThumbnailLength: 631 123 | RatingPercent: 0 124 | Rating: 0 125 | Title: '' 126 | ProfileCMMType: Linotronic 127 | ProfileVersion: 2.1.0 128 | ProfileClass: Display Device Profile 129 | ColorSpaceData: 'RGB ' 130 | ProfileConnectionSpace: 'XYZ ' 131 | ProfileDateTime: 1998-02-09 06:49:00.000000000 +01:00 132 | ProfileFileSignature: acsp 133 | PrimaryPlatform: Microsoft Corporation 134 | CMMFlags: Not Embedded, Independent 135 | DeviceManufacturer: Hewlett-Packard 136 | DeviceModel: sRGB 137 | DeviceAttributes: Reflective, Glossy, Positive, Color 138 | RenderingIntent: Perceptual 139 | ConnectionSpaceIlluminant: 0.9642 1 0.82491 140 | ProfileCreator: Hewlett-Packard 141 | ProfileID: 0 142 | ProfileCopyright: Copyright (c) 1998 Hewlett-Packard Company 143 | ProfileDescription: sRGB IEC61966-2.1 144 | MediaWhitePoint: 0.95045 1 1.08905 145 | MediaBlackPoint: 0 0 0 146 | RedMatrixColumn: 0.43607 0.22249 0.01392 147 | GreenMatrixColumn: 0.38515 0.71687 0.09708 148 | BlueMatrixColumn: 0.14307 0.06061 0.7141 149 | DeviceMfgDesc: IEC http://www.iec.ch 150 | DeviceModelDesc: IEC 61966-2.1 Default RGB colour space - sRGB 151 | ViewingCondDesc: Reference Viewing Condition in IEC61966-2.1 152 | ViewingCondIlluminant: 19.6445 20.3718 16.8089 153 | ViewingCondSurround: 3.92889 4.07439 3.36179 154 | ViewingCondIlluminantType: D50 155 | Luminance: 76.03647 80 87.12462 156 | MeasurementObserver: CIE 1931 157 | MeasurementBacking: 0 0 0 158 | MeasurementGeometry: Unknown 159 | MeasurementFlare: 0.999% 160 | MeasurementIlluminant: D65 161 | Technology: Cathode Ray Tube Display 162 | RedTRC: "(Binary data 2060 bytes, use -b option to extract)" 163 | GreenTRC: "(Binary data 2060 bytes, use -b option to extract)" 164 | BlueTRC: "(Binary data 2060 bytes, use -b option to extract)" 165 | ImageWidth: 1 166 | ImageHeight: 1 167 | EncodingProcess: Baseline DCT, Huffman coding 168 | BitsPerSample: 8 169 | ColorComponents: 3 170 | YCbCrSubSampling: YCbCr4:4:0 (1 2) 171 | Aperture: 5.6 172 | BlueBalance: 7 173 | ImageSize: 1x1 174 | Megapixels: 1.0e-06 175 | RedBalance: 56833 176 | ShutterSpeed: !ruby/object:Rational 177 | denominator: 125 178 | numerator: 1 179 | SubSecCreateDate: 2008-12-12 11:12:30.000000000 +01:00 180 | SubSecDateTimeOriginal: 2008-12-12 11:12:30.000000000 +01:00 181 | SubSecModifyDate: 2012-05-31 21:33:12.000000000 +02:00 182 | ThumbnailImage: "(Binary data 631 bytes, use -b option to extract)" 183 | LensID: AF-S DX VR Zoom-Nikkor 55-200mm f/4-5.6G IF-ED 184 | LensSpec: 55-200mm f/4-5.6 G VR 185 | FocalLength35efl: 55.0 mm 186 | LightValue: 11.8 187 | --- 188 | sample: read Canon.jpg 189 | result: 190 | ExifToolVersion: 13.03 191 | FileSize: 2.7 kB 192 | FileType: JPEG 193 | FileTypeExtension: jpg 194 | MIMEType: image/jpeg 195 | ExifByteOrder: Little-endian (Intel, II) 196 | Make: Canon 197 | Model: Canon EOS DIGITAL REBEL 198 | Orientation: Horizontal (normal) 199 | XResolution: 180 200 | YResolution: 180 201 | ResolutionUnit: inches 202 | ModifyDate: 2003-12-04 06:46:52.000000000 +01:00 203 | YCbCrPositioning: Centered 204 | ExposureTime: 4 205 | FNumber: 14.0 206 | ISO: 100 207 | ExifVersion: '0221' 208 | DateTimeOriginal: 2003-12-04 06:46:52.000000000 +01:00 209 | CreateDate: 2003-12-04 06:46:52.000000000 +01:00 210 | ComponentsConfiguration: Y, Cb, Cr, - 211 | CompressedBitsPerPixel: 9 212 | ShutterSpeedValue: 0 213 | ApertureValue: 14.0 214 | MaxApertureValue: 4.5 215 | Flash: No Flash 216 | FocalLength: 34.0 mm 217 | MacroMode: Unknown (0) 218 | SelfTimer: 'Off' 219 | Quality: RAW 220 | CanonFlashMode: 'Off' 221 | ContinuousDrive: Continuous 222 | FocusMode: Manual Focus (3) 223 | RecordMode: CRW+THM 224 | CanonImageSize: Large 225 | EasyMode: Manual 226 | DigitalZoom: Unknown (-1) 227 | Contrast: "+1" 228 | Saturation: "+1" 229 | Sharpness: "+1" 230 | CameraISO: n/a 231 | MeteringMode: Center-weighted average 232 | FocusRange: Not Known 233 | CanonExposureMode: Manual 234 | LensType: n/a 235 | MaxFocalLength: 55 mm 236 | MinFocalLength: 18 mm 237 | FocalUnits: 1/mm 238 | MaxAperture: 4 239 | MinAperture: 27 240 | FlashActivity: 0 241 | FlashBits: "(none)" 242 | ZoomSourceWidth: 3072 243 | ZoomTargetWidth: 3072 244 | ManualFlashOutput: n/a 245 | ColorTone: Normal 246 | FocalPlaneXSize: 23.22 mm 247 | FocalPlaneYSize: 15.49 mm 248 | AutoISO: 100 249 | BaseISO: 100 250 | MeasuredEV: -1.25 251 | TargetAperture: 14 252 | ExposureCompensation: 0 253 | WhiteBalance: Auto 254 | SlowShutter: None 255 | SequenceNumber: 0 256 | OpticalZoomCode: n/a 257 | FlashGuideNumber: 0 258 | FlashExposureComp: 0 259 | AutoExposureBracketing: 'Off' 260 | AEBBracketValue: 0 261 | ControlMode: Camera Local Control 262 | FocusDistanceUpper: inf 263 | FocusDistanceLower: 5.46 m 264 | MeasuredEV2: -1.25 265 | BulbDuration: 4 266 | CameraType: EOS Mid-range 267 | AutoRotate: None 268 | NDFilter: n/a 269 | SelfTimer2: 0 270 | BracketMode: 'Off' 271 | BracketValue: 0 272 | BracketShotNumber: 0 273 | CanonImageType: CRW:EOS DIGITAL REBEL CMOS RAW 274 | CanonFirmwareVersion: Firmware Version 1.1.1 275 | SerialNumber: '0560018150' 276 | SerialNumberFormat: Format 1 277 | FileNumber: 118-1861 278 | OwnerName: Phil Harvey 279 | CanonModelID: EOS Digital Rebel / 300D / Kiss Digital 280 | CanonFileLength: 4480822 281 | MeasuredRGGB: 998 1022 1026 808 282 | WB_RGGBLevelsAuto: 1719 832 831 990 283 | WB_RGGBLevelsDaylight: 1722 832 831 989 284 | WB_RGGBLevelsShade: 2035 832 831 839 285 | WB_RGGBLevelsCloudy: 1878 832 831 903 286 | WB_RGGBLevelsTungsten: 1228 913 912 1668 287 | WB_RGGBLevelsFluorescent: 1506 842 841 1381 288 | WB_RGGBLevelsFlash: 1933 832 831 895 289 | WB_RGGBLevelsCustom: 1722 832 831 989 290 | WB_RGGBLevelsKelvin: 1722 832 831 988 291 | WB_RGGBBlackLevels: 124 123 124 123 292 | ColorTemperature: 5200 293 | NumAFPoints: 7 294 | ValidAFPoints: 7 295 | CanonImageWidth: 3072 296 | CanonImageHeight: 2048 297 | AFImageWidth: 3072 298 | AFImageHeight: 2048 299 | AFAreaWidth: 151 300 | AFAreaHeight: 151 301 | AFAreaXPositions: 1014 608 0 0 0 -608 -1014 302 | AFAreaYPositions: 0 0 -506 0 506 0 0 303 | AFPointsInFocus: "(none)" 304 | ThumbnailImageValidArea: 0 159 7 112 305 | UserComment: '' 306 | FlashpixVersion: '0100' 307 | ColorSpace: sRGB 308 | ExifImageWidth: 160 309 | ExifImageHeight: 120 310 | InteropIndex: THM - DCF thumbnail file 311 | InteropVersion: '0100' 312 | RelatedImageWidth: 3072 313 | RelatedImageHeight: 2048 314 | FocalPlaneXResolution: 3443.946188 315 | FocalPlaneYResolution: 3442.016807 316 | FocalPlaneResolutionUnit: inches 317 | SensingMethod: One-chip color area 318 | FileSource: Digital Camera 319 | CustomRendered: Normal 320 | ExposureMode: Manual 321 | SceneCaptureType: Standard 322 | ImageWidth: 8 323 | ImageHeight: 8 324 | EncodingProcess: Baseline DCT, Huffman coding 325 | BitsPerSample: 8 326 | ColorComponents: 3 327 | YCbCrSubSampling: YCbCr4:2:0 (2 2) 328 | DriveMode: Continuous Shooting 329 | Lens: 18.0 - 55.0 mm 330 | ShootingMode: Bulb 331 | WB_RGGBLevels: 1719 832 831 990 332 | Aperture: 14.0 333 | BlueBalance: 1.190619 334 | ImageSize: 8x8 335 | LensID: Unknown 18-55mm 336 | Megapixels: 6.4e-05 337 | RedBalance: 2.067348 338 | ScaleFactor35efl: 1.6 339 | ShutterSpeed: 4 340 | Lens35efl: '18.0 - 55.0 mm (35 mm equivalent: 28.6 - 87.4 mm)' 341 | CircleOfConfusion: 0.019 mm 342 | DOF: inf (4.31 m - inf) 343 | FOV: 36.9 deg 344 | FocalLength35efl: '34.0 mm (35 mm equivalent: 54.0 mm)' 345 | HyperfocalDistance: 4.37 m 346 | LightValue: 5.6 347 | --- 348 | sample: read test.jpg 349 | result: 350 | ExifToolVersion: 13.03 351 | FileSize: 48 kB 352 | FileType: JPEG 353 | FileTypeExtension: jpg 354 | MIMEType: image/jpeg 355 | JFIFVersion: 1.01 356 | ExifByteOrder: Big-endian (Motorola, MM) 357 | ImageDescription: KONICA MINOLTA DIGITAL CAMERA 358 | Make: KONICA MINOLTA 359 | Model: DYNAX 7D 360 | Orientation: Horizontal (normal) 361 | XResolution: 72 362 | YResolution: 72 363 | ResolutionUnit: inches 364 | Software: DYNAX 7D v1.10 365 | ModifyDate: 2005-09-13 20:08:50.000000000 +02:00 366 | YCbCrPositioning: Centered 367 | ExposureTime: !ruby/object:Rational 368 | denominator: 60 369 | numerator: 1 370 | FNumber: 9.5 371 | ExposureProgram: Program AE 372 | ISO: 400 373 | ExifVersion: '0221' 374 | DateTimeOriginal: 2005-09-13 20:08:50.000000000 +02:00 375 | CreateDate: 2005-09-13 20:08:50.000000000 +02:00 376 | ComponentsConfiguration: Y, Cb, Cr, - 377 | BrightnessValue: 4.5 378 | ExposureCompensation: -1 379 | MaxApertureValue: 4.5 380 | MeteringMode: Multi-segment 381 | LightSource: Unknown 382 | Flash: Off, Did not fire 383 | FocalLength: 75.0 mm 384 | SubjectArea: 1504 1000 256 304 385 | MakerNoteVersion: MLT0 386 | MinoltaImageSize: Large 387 | WhiteBalance: Auto 388 | FocusMode: AF-A 389 | AFPoints: Center 390 | FlashMode: Normal 391 | ISOSetting: 400 392 | FreeMemoryCardImages: 202 393 | HueAdjustment: 0 394 | Rotation: Horizontal (normal) 395 | ImageNumber: 6 396 | NoiseReduction: Unknown (2) 397 | ImageNumber2: 50 398 | ZoneMatchingOn: 'Off' 399 | CompressedImageSize: 1598477 400 | PreviewImageStart: 39152 401 | PreviewImageLength: 0 402 | SceneMode: Standard 403 | ColorMode: Natural sRGB 404 | MinoltaQuality: Fine 405 | FlashExposureComp: 0 406 | Teleconverter: None 407 | ImageStabilization: 'On' 408 | ZoneMatching: ISO Setting Used 409 | ColorTemperature: 0 410 | LensType: Minolta AF 28-135mm F4-4.5 or Other Lens 411 | UserComment: '' 412 | FlashpixVersion: '0100' 413 | ColorSpace: sRGB 414 | ExifImageWidth: 3008 415 | ExifImageHeight: 2000 416 | CustomRendered: Normal 417 | ExposureMode: Auto 418 | DigitalZoomRatio: 0 419 | FocalLengthIn35mmFormat: 112 mm 420 | SceneCaptureType: Standard 421 | GainControl: Low gain up 422 | Contrast: Normal 423 | Saturation: Normal 424 | Sharpness: Normal 425 | PrintIMVersion: '0300' 426 | Compression: JPEG (old-style) 427 | ThumbnailOffset: 39274 428 | ThumbnailLength: 1820 429 | CurrentIPTCDigest: dd8d51d28ddf04f08f870e5ff2f64d01 430 | Keywords: 431 | - Orange 432 | - Rot 433 | ApplicationRecordVersion: 4 434 | SupplementalCategories: Natur 435 | XMPToolkit: Image::ExifTool 7.03 436 | Title: Abenddämmerung 437 | ImageWidth: 300 438 | ImageHeight: 199 439 | EncodingProcess: Baseline DCT, Huffman coding 440 | BitsPerSample: 8 441 | ColorComponents: 3 442 | YCbCrSubSampling: YCbCr4:2:0 (2 2) 443 | Aperture: 9.5 444 | ImageSize: 300x199 445 | LensID: Tokina AT-X 242 AF 24-200mm F3.5-5.6 446 | Megapixels: 0.06 447 | ScaleFactor35efl: 1.5 448 | ShutterSpeed: !ruby/object:Rational 449 | denominator: 60 450 | numerator: 1 451 | ThumbnailImage: "(Binary data 1820 bytes, use -b option to extract)" 452 | CircleOfConfusion: 0.020 mm 453 | FOV: 18.3 deg 454 | FocalLength35efl: '75.0 mm (35 mm equivalent: 112.0 mm)' 455 | HyperfocalDistance: 29.43 m 456 | LightValue: 10.4 457 | --- 458 | sample: read test_coordinates.jpg 459 | result: 460 | ExifToolVersion: 13.03 461 | FileSize: 257 kB 462 | FileType: JPEG 463 | FileTypeExtension: jpg 464 | MIMEType: image/jpeg 465 | ExifByteOrder: Big-endian (Motorola, MM) 466 | ImageDescription: clip 467 | Orientation: Horizontal (normal) 468 | XResolution: 72 469 | YResolution: 72 470 | ResolutionUnit: inches 471 | YCbCrPositioning: Centered 472 | ExposureTime: !ruby/object:Rational 473 | denominator: 30 474 | numerator: 1 475 | FNumber: 2.4 476 | ExposureProgram: Program AE 477 | ISO: 400 478 | ExifVersion: '0221' 479 | ComponentsConfiguration: Y, Cb, Cr, - 480 | ShutterSpeedValue: !ruby/object:Rational 481 | denominator: 30 482 | numerator: 1 483 | ApertureValue: 2.4 484 | BrightnessValue: 1.954376479 485 | MeteringMode: Multi-segment 486 | Flash: Off, Did not fire 487 | FocalLength: 4.1 mm 488 | SubjectArea: 519 403 239 180 489 | UserComment: clip 490 | FlashpixVersion: '0100' 491 | ColorSpace: sRGB 492 | ExifImageWidth: 1280 493 | ExifImageHeight: 720 494 | SensingMethod: One-chip color area 495 | ExposureMode: Auto 496 | WhiteBalance: Auto 497 | FocalLengthIn35mmFormat: 35 mm 498 | SceneCaptureType: Standard 499 | GPSLatitudeRef: North 500 | GPSLongitudeRef: West 501 | GPSAltitude: 0 m 502 | GPSTimeStamp: '00:00:00' 503 | GPSDOP: 0 504 | Compression: JPEG (old-style) 505 | ThumbnailOffset: 766 506 | ThumbnailLength: 3831 507 | ImageWidth: 1280 508 | ImageHeight: 720 509 | EncodingProcess: Baseline DCT, Huffman coding 510 | BitsPerSample: 8 511 | ColorComponents: 3 512 | YCbCrSubSampling: YCbCr4:2:0 (2 2) 513 | Aperture: 2.4 514 | ImageSize: 1280x720 515 | Megapixels: 0.922 516 | ScaleFactor35efl: 8.5 517 | ShutterSpeed: !ruby/object:Rational 518 | denominator: 30 519 | numerator: 1 520 | ThumbnailImage: "(Binary data 3831 bytes, use -b option to extract)" 521 | GPSLatitude: 43 deg 39' 11.40" N 522 | GPSLongitude: 79 deg 22' 23.40" W 523 | CircleOfConfusion: 0.004 mm 524 | FOV: 54.4 deg 525 | FocalLength35efl: '4.1 mm (35 mm equivalent: 35.0 mm)' 526 | GPSPosition: 43 deg 39' 11.40" N, 79 deg 22' 23.40" W 527 | HyperfocalDistance: 2.00 m 528 | LightValue: 5.4 529 | --- 530 | sample: read test_encodings.jpg 531 | result: 532 | ExifToolVersion: 13.03 533 | FileSize: 660 bytes 534 | FileType: JPEG 535 | FileTypeExtension: jpg 536 | MIMEType: image/jpeg 537 | CurrentIPTCDigest: '09b79ab4703f3570ba3140ffc2dba2f6' 538 | ObjectName: Mšhre 539 | ApplicationRecordVersion: 4 540 | ImageWidth: 1 541 | ImageHeight: 1 542 | EncodingProcess: Baseline DCT, Huffman coding 543 | BitsPerSample: 8 544 | ColorComponents: 3 545 | YCbCrSubSampling: YCbCr4:2:0 (2 2) 546 | ImageSize: 1x1 547 | Megapixels: 1.0e-06 548 | --- 549 | sample: read test_special_dates.jpg 550 | result: 551 | ExifToolVersion: 13.03 552 | FileSize: 3.8 kB 553 | FileType: JPEG 554 | FileTypeExtension: jpg 555 | MIMEType: image/jpeg 556 | ExifByteOrder: Big-endian (Motorola, MM) 557 | XResolution: 72 558 | YResolution: 72 559 | ResolutionUnit: inches 560 | ModifyDate: false 561 | YCbCrPositioning: Centered 562 | ExifVersion: '0220' 563 | DateTimeOriginal: 1961-08-13 12:08:25.000000000 +01:00 564 | ComponentsConfiguration: Y, Cb, Cr, - 565 | FlashpixVersion: '0100' 566 | ColorSpace: Uncalibrated 567 | PreviewDateTime: 1961-08-13 12:08:25.000000000 +01:00 568 | ImageWidth: 300 569 | ImageHeight: 199 570 | EncodingProcess: Baseline DCT, Huffman coding 571 | BitsPerSample: 8 572 | ColorComponents: 3 573 | YCbCrSubSampling: YCbCr4:2:0 (2 2) 574 | ImageSize: 300x199 575 | Megapixels: 0.06 576 | -------------------------------------------------------------------------------- /test/data/Bad_PreviewIFD.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janfri/mini_exiftool/cfb221a06440182a10d7fbafc29d5de0a1e1bca0/test/data/Bad_PreviewIFD.jpg -------------------------------------------------------------------------------- /test/data/Canon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janfri/mini_exiftool/cfb221a06440182a10d7fbafc29d5de0a1e1bca0/test/data/Canon.jpg -------------------------------------------------------------------------------- /test/data/INFORMATION: -------------------------------------------------------------------------------- 1 | Following files are borrowed from the original ExifTool perl package. 2 | 3 | Canon.jpg -------------------------------------------------------------------------------- /test/data/invalid_byte_sequence_in_utf8.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janfri/mini_exiftool/cfb221a06440182a10d7fbafc29d5de0a1e1bca0/test/data/invalid_byte_sequence_in_utf8.json -------------------------------------------------------------------------------- /test/data/invalid_rational.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "UserComment": "1/0" 3 | }] 4 | -------------------------------------------------------------------------------- /test/data/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janfri/mini_exiftool/cfb221a06440182a10d7fbafc29d5de0a1e1bca0/test/data/test.jpg -------------------------------------------------------------------------------- /test/data/test.jpg.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "SourceFile": "test/data/test.jpg", 3 | "ExifToolVersion": 8.77, 4 | "FileName": "test.jpg", 5 | "Directory": "test/data", 6 | "FileSize": "46 kB", 7 | "FileModifyDate": "2012:07:05 20:28:24-07:00", 8 | "FilePermissions": "rw-r--r--", 9 | "FileType": "JPEG", 10 | "MIMEType": "image/jpeg", 11 | "JFIFVersion": 1.01, 12 | "ExifByteOrder": "Big-endian (Motorola, MM)", 13 | "ImageDescription": "KONICA MINOLTA DIGITAL CAMERA", 14 | "Make": "KONICA MINOLTA", 15 | "Model": "DYNAX 7D", 16 | "Orientation": "Horizontal (normal)", 17 | "XResolution": 72, 18 | "YResolution": 72, 19 | "ResolutionUnit": "inches", 20 | "Software": "DYNAX 7D v1.10", 21 | "ModifyDate": "2005:09:13 20:08:50", 22 | "YCbCrPositioning": "Centered", 23 | "ExposureTime": "1/60", 24 | "FNumber": 9.5, 25 | "ExposureProgram": "Program AE", 26 | "ISO": 400, 27 | "ExifVersion": "0221", 28 | "DateTimeOriginal": "2005:09:13 20:08:50", 29 | "CreateDate": "2005:09:13 20:08:50", 30 | "ComponentsConfiguration": "Y, Cb, Cr, -", 31 | "BrightnessValue": 4.5, 32 | "ExposureCompensation": -1, 33 | "MaxApertureValue": 4.5, 34 | "MeteringMode": "Multi-segment", 35 | "LightSource": "Unknown", 36 | "Flash": "Off, Did not fire", 37 | "FocalLength": "75.0 mm", 38 | "SubjectArea": "1504 1000 256 304", 39 | "MakerNoteVersion": "MLT0", 40 | "MinoltaImageSize": "Large", 41 | "WhiteBalance": "Auto", 42 | "FocusMode": "AF-A", 43 | "AFPoints": "Center", 44 | "FlashMode": "Normal", 45 | "ISOSetting": 400, 46 | "FreeMemoryCardImages": 202, 47 | "HueAdjustment": 0, 48 | "Rotation": "Horizontal (normal)", 49 | "ImageNumber": 6, 50 | "NoiseReduction": "Unknown (2)", 51 | "ImageNumber2": 50, 52 | "ZoneMatchingOn": "Off", 53 | "CompressedImageSize": 1598477, 54 | "PreviewImageStart": 39152, 55 | "PreviewImageLength": 0, 56 | "SceneMode": "Standard", 57 | "ColorMode": "Natural sRGB", 58 | "MinoltaQuality": "Fine", 59 | "FlashExposureComp": 0, 60 | "Teleconverter": "None", 61 | "ImageStabilization": "On", 62 | "ZoneMatching": "ISO Setting Used", 63 | "ColorTemperature": 0, 64 | "LensType": "Minolta AF 28-135mm F4-4.5 or Sigma Lens", 65 | "UserComment": "", 66 | "FlashpixVersion": "0100", 67 | "ColorSpace": "sRGB", 68 | "ExifImageWidth": 3008, 69 | "ExifImageHeight": 2000, 70 | "CustomRendered": "Normal", 71 | "ExposureMode": "Auto", 72 | "DigitalZoomRatio": 0, 73 | "FocalLengthIn35mmFormat": "112 mm", 74 | "SceneCaptureType": "Standard", 75 | "GainControl": "Low gain up", 76 | "Contrast": "Normal", 77 | "Saturation": "Normal", 78 | "Sharpness": "Normal", 79 | "PrintIMVersion": "0300", 80 | "Compression": "JPEG (old-style)", 81 | "ThumbnailOffset": 39274, 82 | "ThumbnailLength": 1820, 83 | "CurrentIPTCDigest": "dd8d51d28ddf04f08f870e5ff2f64d01", 84 | "Keywords": ["Orange","Rot"], 85 | "ApplicationRecordVersion": 4, 86 | "SupplementalCategories": "Natur", 87 | "XMPToolkit": "Image::ExifTool 7.03", 88 | "Title": "Abenddämmerung", 89 | "ImageWidth": 300, 90 | "ImageHeight": 199, 91 | "EncodingProcess": "Baseline DCT, Huffman coding", 92 | "BitsPerSample": 8, 93 | "ColorComponents": 3, 94 | "YCbCrSubSampling": "YCbCr4:2:0 (2 2)", 95 | "Aperture": 9.5, 96 | "ImageSize": "300x199", 97 | "LensID": "Minolta AF 28-135mm F4-4.5", 98 | "ScaleFactor35efl": 1.5, 99 | "ShutterSpeed": "1/60", 100 | "ThumbnailImage": "(Binary data 1820 bytes)", 101 | "CircleOfConfusion": "0.020 mm", 102 | "FOV": "18.3 deg", 103 | "FocalLength35efl": "75.0 mm (35 mm equivalent: 112.0 mm)", 104 | "HyperfocalDistance": "29.43 m", 105 | "LightValue": 10.4 106 | }] 107 | -------------------------------------------------------------------------------- /test/data/test_coordinates.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janfri/mini_exiftool/cfb221a06440182a10d7fbafc29d5de0a1e1bca0/test/data/test_coordinates.jpg -------------------------------------------------------------------------------- /test/data/test_encodings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janfri/mini_exiftool/cfb221a06440182a10d7fbafc29d5de0a1e1bca0/test/data/test_encodings.jpg -------------------------------------------------------------------------------- /test/data/test_special_dates.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janfri/mini_exiftool/cfb221a06440182a10d7fbafc29d5de0a1e1bca0/test/data/test_special_dates.jpg -------------------------------------------------------------------------------- /test/helpers_for_test.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'mini_exiftool' 3 | require 'test/unit' 4 | require 'fileutils' 5 | require 'tempfile' 6 | require 'yaml' 7 | 8 | include Test::Unit 9 | 10 | module TempfileTest 11 | def setup 12 | @temp_file = Tempfile.new('test') 13 | @temp_filename = @temp_file.path 14 | @data_dir = File.dirname(__FILE__) + '/data' 15 | end 16 | 17 | def teardown 18 | @temp_file.close 19 | end 20 | 21 | def assert_md5 md5, filename 22 | assert_equal md5, Digest::MD5.hexdigest(File.read(filename)) 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /test/test_bad_preview_ifd.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestBadPreviewIFD < TestCase 5 | 6 | include TempfileTest 7 | 8 | def setup 9 | super 10 | @org_filename = @data_dir + '/Bad_PreviewIFD.jpg' 11 | FileUtils.cp @org_filename, @temp_filename 12 | @bad_preview_ifd = MiniExiftool.new @temp_filename 13 | end 14 | 15 | # Feature request rubyforge [#29587] 16 | # Thanks to Michael Grove for reporting 17 | def test_m_option 18 | title = 'anything' 19 | @bad_preview_ifd.title = title 20 | assert_equal false, @bad_preview_ifd.save, '-m option seems to be not neccessary' 21 | @bad_preview_ifd.reload 22 | @bad_preview_ifd.title = title 23 | @bad_preview_ifd.ignore_minor_errors = true 24 | assert_equal true, @bad_preview_ifd.save, 'Error while saving' 25 | @bad_preview_ifd.reload 26 | assert_equal title, @bad_preview_ifd.title 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /test/test_class_methods.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestClassMethods < TestCase 5 | 6 | def test_new 7 | assert_nothing_raised do 8 | @mini_exiftool = MiniExiftool.new 9 | end 10 | assert_equal nil, @mini_exiftool.filename 11 | assert_nothing_raised do 12 | MiniExiftool.new nil 13 | end 14 | assert_raises MiniExiftool::Error do 15 | MiniExiftool.new false 16 | end 17 | assert_raises MiniExiftool::Error do 18 | MiniExiftool.new '' 19 | end 20 | assert_raises MiniExiftool::Error do 21 | MiniExiftool.new 'not_existing_file' 22 | end 23 | assert_raises MiniExiftool::Error do 24 | MiniExiftool.new '.' # directory 25 | end 26 | begin 27 | MiniExiftool.new 'not_existing_file' 28 | rescue MiniExiftool::Error => e 29 | assert_match /File 'not_existing_file' does not exist/, e.message 30 | end 31 | end 32 | 33 | def test_command 34 | cmd = MiniExiftool.command 35 | assert_equal 'exiftool', cmd 36 | MiniExiftool.command = 'non_existend' 37 | assert_equal 'non_existend', MiniExiftool.command 38 | assert_raises MiniExiftool::Error do 39 | met = MiniExiftool.new(File.join(File.dirname(__FILE__), 40 | 'data/test.jpg')) 41 | end 42 | MiniExiftool.command = cmd 43 | end 44 | 45 | def test_opts 46 | opts = MiniExiftool.opts 47 | assert_kind_of Hash, opts 48 | begin 49 | org = MiniExiftool.opts[:composite] 50 | met1 = MiniExiftool.new 51 | MiniExiftool.opts[:composite] = !org 52 | met2 = MiniExiftool.new 53 | MiniExiftool.opts[:composite] = org 54 | met3 = MiniExiftool.new 55 | assert_equal org, met1.composite 56 | assert_equal !org, met2.composite 57 | assert_equal org, met1.composite 58 | ensure 59 | MiniExiftool.opts[:composite] = org 60 | end 61 | end 62 | 63 | def test_all_tags 64 | all_tags = MiniExiftool.all_tags 65 | assert all_tags.include?('ISO') 66 | assert all_tags.include?('ExifToolVersion') 67 | end 68 | 69 | def test_writable_tags 70 | w_tags = MiniExiftool.writable_tags 71 | assert w_tags.include?('ISO') 72 | assert ! w_tags.include?('ExifToolVersion') 73 | end 74 | 75 | def test_exiftool_version 76 | v = MiniExiftool.exiftool_version 77 | assert_match /\A\d+\.\d+\z/, v 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /test/test_composite.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestComposite < TestCase 5 | 6 | def setup 7 | @data_dir = File.dirname(__FILE__) + '/data' 8 | @filename_test = @data_dir + '/test.jpg' 9 | @mini_exiftool = MiniExiftool.new @filename_test, :composite => false 10 | @mini_exiftool_c = MiniExiftool.new @filename_test 11 | end 12 | 13 | def test_composite_tags 14 | assert_equal false, @mini_exiftool.tags.include?('Aperture') 15 | assert_equal true, @mini_exiftool_c.tags.include?('Aperture') 16 | assert_equal 9.5, @mini_exiftool_c['Aperture'] 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /test/test_copy_tags_from.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestCopyTagsFrom < TestCase 5 | 6 | include TempfileTest 7 | 8 | def setup 9 | super 10 | @canon_filename = @data_dir + '/Canon.jpg' 11 | FileUtils.cp(@canon_filename, @temp_filename) 12 | @mini_exiftool = MiniExiftool.new(@temp_filename) 13 | @source_filename = @data_dir + '/test.jpg' 14 | @canon_md5 = Digest::MD5.hexdigest(File.read(@canon_filename)) 15 | @source_md5 = Digest::MD5.hexdigest(File.read(@source_filename)) 16 | end 17 | 18 | def test_single_tag 19 | assert_nil @mini_exiftool.title 20 | res = @mini_exiftool.copy_tags_from(@source_filename, :title) 21 | assert res 22 | assert_equal 'Abenddämmerung', @mini_exiftool.title 23 | assert_md5 @source_md5, @source_filename 24 | end 25 | 26 | def test_more_than_one_tag 27 | assert_nil @mini_exiftool.title 28 | assert_nil @mini_exiftool.keywords 29 | res = @mini_exiftool.copy_tags_from(@source_filename, %w[title keywords]) 30 | assert res 31 | assert_equal 'Abenddämmerung', @mini_exiftool.title 32 | assert_equal %w[Orange Rot], @mini_exiftool.keywords 33 | assert_md5 @source_md5, @source_filename 34 | end 35 | 36 | def test_non_existing_sourcefile 37 | assert_raises MiniExiftool::Error do 38 | @mini_exiftool.copy_tags_from('non_existend_file', :title) 39 | end 40 | assert_md5 @source_md5, @source_filename 41 | end 42 | 43 | def test_non_existend_tag 44 | @mini_exiftool.copy_tags_from(@source_filename, :non_existend_tag) 45 | assert_md5 @canon_md5, @canon_filename 46 | assert_md5 @source_md5, @source_filename 47 | end 48 | 49 | def test_non_writable_tag 50 | @mini_exiftool.copy_tags_from(@source_filename, 'JFIF') 51 | assert_md5 @canon_md5, @canon_filename 52 | assert_md5 @source_md5, @source_filename 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /test/test_dumping.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestDumping < TestCase 5 | 6 | def setup 7 | @data_dir = File.dirname(__FILE__) + '/data' 8 | @filename_test = @data_dir + '/test.jpg' 9 | @mini_exiftool = MiniExiftool.new @filename_test 10 | end 11 | 12 | def test_to_hash 13 | hash = @mini_exiftool.to_hash 14 | assert_equal Hash, hash.class 15 | assert_equal @mini_exiftool.tags.size, hash.size, 'Size of Hash is not correct.' 16 | assert_not_nil hash['ExifToolVersion'], 'Original name of exiftool tag is not preserved.' 17 | all_ok = true 18 | different_tag = '' 19 | v = '' 20 | hash.each do |k,v| 21 | unless @mini_exiftool[k] == v 22 | all_ok = false 23 | different_tag = k 24 | break 25 | end 26 | end 27 | assert all_ok, "Tag #{different_tag.inspect}: expected: #{@mini_exiftool[different_tag].inspect}, actual: #{v.inspect}" 28 | end 29 | 30 | def test_from_hash 31 | hash = @mini_exiftool.to_hash 32 | mini_exiftool_new = MiniExiftool.from_hash hash 33 | assert_equal MiniExiftool, mini_exiftool_new.class 34 | assert_equal @mini_exiftool.tags.size, mini_exiftool_new.tags.size 35 | all_ok = true 36 | different_tag = '' 37 | @mini_exiftool.tags.each do |tag| 38 | unless @mini_exiftool[tag] == mini_exiftool_new[tag] 39 | all_ok = false 40 | different_tag = tag 41 | break 42 | end 43 | end 44 | assert all_ok, "Tag #{different_tag.inspect}: expected: #{@mini_exiftool[different_tag].inspect}, actual: #{mini_exiftool_new[different_tag].inspect}" 45 | 46 | end 47 | 48 | def test_to_yaml 49 | hash = @mini_exiftool.to_hash 50 | yaml = @mini_exiftool.to_yaml 51 | assert_equal hash, YAML.unsafe_load(yaml) 52 | end 53 | 54 | def test_from_yaml 55 | hash = @mini_exiftool.to_hash 56 | yaml = hash.to_yaml 57 | mini_exiftool_new = MiniExiftool.from_yaml(yaml) 58 | assert_equal MiniExiftool, mini_exiftool_new.class 59 | assert_equal hash, mini_exiftool_new.to_hash 60 | end 61 | 62 | def test_heuristics_for_restoring_composite 63 | standard = @mini_exiftool.to_hash 64 | no_composite = MiniExiftool.new(@filename_test, :composite => false).to_hash 65 | assert_equal true, MiniExiftool.from_hash(standard).composite 66 | assert_equal false, MiniExiftool.from_hash(no_composite).composite 67 | assert_equal true, MiniExiftool.from_yaml(standard.to_yaml).composite 68 | assert_equal false, MiniExiftool.from_yaml(no_composite.to_yaml).composite 69 | end 70 | 71 | def test_heuristics_for_restoring_numerical 72 | standard = @mini_exiftool.to_hash 73 | numerical = MiniExiftool.new(@filename_test, :numerical => true).to_hash 74 | assert_equal false, MiniExiftool.from_hash(standard).numerical 75 | assert_equal true, MiniExiftool.from_hash(numerical).numerical 76 | assert_equal false, MiniExiftool.from_yaml(standard.to_yaml).numerical 77 | assert_equal true, MiniExiftool.from_yaml(numerical.to_yaml).numerical 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /test/test_encodings.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestEncodings < TestCase 5 | 6 | include TempfileTest 7 | 8 | def setup 9 | super 10 | @data_dir = File.dirname(__FILE__) + '/data' 11 | @filename_test = @data_dir + '/test_encodings.jpg' 12 | @mini_exiftool = MiniExiftool.new @filename_test 13 | end 14 | 15 | def test_iptc_encoding 16 | object_name = "Möhre" 17 | assert_not_equal object_name, @mini_exiftool.object_name 18 | correct_iptc = MiniExiftool.new(@filename_test, iptc_encoding: 'MacRoman') 19 | assert_equal object_name, correct_iptc.object_name 20 | FileUtils.cp(@filename_test, @temp_filename) 21 | correct_iptc_write = MiniExiftool.new(@temp_filename, iptc_encoding: 'MacRoman') 22 | caption = 'Das ist eine Möhre' 23 | correct_iptc_write.caption_abstract = caption 24 | correct_iptc_write.save! 25 | correct_iptc_write.reload 26 | assert_equal object_name, correct_iptc_write.object_name 27 | assert_equal caption, correct_iptc_write.caption_abstract 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /test/test_filename_access.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | require 'rbconfig' 4 | require 'tmpdir' 5 | 6 | # Thanks to uwe58 and others for hints 7 | 8 | class TestFilenameAccess < TestCase 9 | 10 | @@running_on_windows = /mswin|mingw|cygwin/ === RbConfig::CONFIG['host_os'] 11 | 12 | @@fs_enc = Encoding.find('filesystem') 13 | 14 | def create_testfile(basename_new) 15 | tmpdir = Dir.tmpdir 16 | filename_org = File.join(File.dirname(__FILE__), 'data/test.jpg') 17 | filename_new = File.join(tmpdir, basename_new) 18 | FileUtils.cp filename_org, filename_new.encode(@@fs_enc) 19 | filename_new 20 | end 21 | 22 | def do_testing_with(basename) 23 | filename_test = create_testfile(basename) 24 | # read 25 | m = MiniExiftool.new filename_test 26 | assert_equal 400, m.iso 27 | # save 28 | m.iso = 200 29 | m.save 30 | assert_equal 200, m.iso 31 | # Check original filename maybe with other encoding than filesystem 32 | assert_equal basename, File.basename(m.filename) 33 | rescue Exception => e 34 | assert false, "File #{filename_test.inspect} not found!" 35 | end 36 | 37 | def test_access_filename_with_spaces 38 | do_testing_with 'filename with spaces.jpg' 39 | end 40 | 41 | def test_access_filename_with_special_chars 42 | do_testing_with 'filename_with_Ümläüts.jpg' 43 | end 44 | 45 | unless @@running_on_windows 46 | def test_access_filename_with_doublequotes 47 | do_testing_with 'filename_with_"doublequotes"_inside.jpg' 48 | end 49 | end 50 | 51 | def test_access_filename_with_dollar_sign 52 | # Thanks to Michael Dungan for the hint 53 | do_testing_with 'filename_with_$_sign.jpg' 54 | end 55 | 56 | def test_access_filename_with_ampersand 57 | do_testing_with 'filename_with_&_sign.jpg' 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /test/test_from_hash.rb: -------------------------------------------------------------------------------- 1 | require 'helpers_for_test' 2 | require 'json' 3 | 4 | class TestFromHash < TestCase 5 | def setup 6 | @data_dir = File.dirname(__FILE__) + '/data' 7 | hash_data = JSON.parse(File.read( @data_dir + '/test.jpg.json')).first 8 | @mini_exiftool = MiniExiftool.from_hash hash_data 9 | end 10 | 11 | def test_conversion 12 | assert_kind_of String, @mini_exiftool.model 13 | assert_kind_of Time, @mini_exiftool['DateTimeOriginal'] 14 | assert_kind_of Float, @mini_exiftool['MaxApertureValue'] 15 | assert_kind_of String, @mini_exiftool.flash 16 | assert_kind_of Integer, @mini_exiftool['ExposureCompensation'] 17 | assert_kind_of String, (@mini_exiftool['SubjectLocation'] || @mini_exiftool['SubjectArea']) 18 | assert_kind_of Array, @mini_exiftool['Keywords'] 19 | assert_kind_of String, @mini_exiftool['SupplementalCategories'] 20 | assert_kind_of Rational, @mini_exiftool.shutterspeed 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/test_instance_methods.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestRead < TestCase 5 | 6 | def setup 7 | @data_dir = File.dirname(__FILE__) + '/data' 8 | @filename_test = @data_dir + '/test.jpg' 9 | @mini_exiftool = MiniExiftool.new @filename_test 10 | end 11 | 12 | def test_respond_to_missing 13 | assert_true @mini_exiftool.respond_to?(:iso), 'instance should respond to iso because it has a value for ISO' 14 | assert_true @mini_exiftool.respond_to?('iso'), 'instance should respond to iso because it has a value for ISO' 15 | assert_true @mini_exiftool.respond_to?('ISO'), 'instance should respond to ISO because it has a value for ISO' 16 | assert_true @mini_exiftool.respond_to?(:iso=), 'instance should respond to iso= because all setters are allowed' 17 | assert_true @mini_exiftool.respond_to?('iso='), 'instance should respond to iso= because all setters are allowed' 18 | assert_true @mini_exiftool.respond_to?('ISO='), 'instance should respond to ISO= because all setters are allowed' 19 | assert_false @mini_exiftool.respond_to?(:comment), 'instance should not respond to comment because it has no value for Comment' 20 | assert_false @mini_exiftool.respond_to?('comment'), 'instance should not respond to comment because it has no value for Comment' 21 | assert_false @mini_exiftool.respond_to?('Comment'), 'instance should not respond to Comment because it has no value for Comment' 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /test/test_invalid_byte_sequence_in_utf8.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | require 'json' 4 | 5 | # Thanks to Chris Salzberg (aka shioyama) and 6 | # Robert May (aka robotmay) for precious hints 7 | 8 | class TestInvalidByteSequenceInUtf8 < TestCase 9 | 10 | def setup 11 | @json = File.read(File.dirname(__FILE__) + '/data/invalid_byte_sequence_in_utf8.json') 12 | end 13 | 14 | def test_invalid_byte_sequence_gets_unconverted_value_with_invalid_encoding 15 | omit 'Java version of json gem can not handle invalid encoded data' if RUBY_PLATFORM == 'java' 16 | assert_nothing_raised do 17 | mini_exiftool = MiniExiftool.from_json(@json) 18 | assert_equal 1561, mini_exiftool.color_balance_unknown.size 19 | end 20 | end 21 | 22 | def test_replace_invalid_chars 23 | assert_nothing_raised do 24 | mini_exiftool = MiniExiftool.from_json(@json, :replace_invalid_chars => '') 25 | assert_equal 1036, mini_exiftool.color_balance_unknown.size 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /test/test_invalid_rational.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestInvalidRational < TestCase 5 | 6 | def test_rescue_from_invalid_rational 7 | mini_exiftool = MiniExiftool.from_json(File.read('test/data/invalid_rational.json')) 8 | assert_equal '1/0', mini_exiftool.user_comment 9 | rescue Exception 10 | assert false, 'Tag values of the form x/0 should not raise an Exception.' 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /test/test_io.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | require 'stringio' 4 | 5 | class TestIo < TestCase 6 | 7 | def test_simple_case 8 | io = open_real_io 9 | mini_exiftool = MiniExiftool.new(io) 10 | assert_equal false, io.closed?, 'IO should not be closed.' 11 | assert_equal 400, mini_exiftool.iso 12 | end 13 | 14 | def test_non_readable_io 15 | assert_raises MiniExiftool::Error do 16 | begin 17 | MiniExiftool.new($stdout) 18 | rescue MiniExiftool::Error => e 19 | assert_equal 'IO is not readable.', e.message 20 | raise e 21 | end 22 | end 23 | end 24 | 25 | def test_no_writing_when_using_io 26 | io = open_real_io 27 | m = MiniExiftool.new(io) 28 | m.iso = 100 29 | assert_raises MiniExiftool::Error do 30 | begin 31 | m.save 32 | rescue MiniExiftool::Error => e 33 | assert_equal 'No writing support when using an IO.', e.message 34 | raise e 35 | end 36 | end 37 | end 38 | 39 | def test_fast_options 40 | $DEBUG = true 41 | s = StringIO.new 42 | $stderr = s 43 | MiniExiftool.new open_real_io 44 | s.rewind 45 | assert_match /^exiftool -j "-"$/, s.read 46 | s = StringIO.new 47 | $stderr = s 48 | MiniExiftool.new open_real_io, :fast => true 49 | s.rewind 50 | assert_match /^exiftool -j -fast "-"$/, s.read 51 | s = StringIO.new 52 | $stderr = s 53 | MiniExiftool.new open_real_io, :fast2 => true 54 | s.rewind 55 | assert_match /^exiftool -j -fast2 "-"$/, s.read 56 | ensure 57 | $DEBUG = false 58 | $stderr = STDERR 59 | end 60 | 61 | protected 62 | 63 | def open_real_io 64 | File.open(File.join(File.dirname(__FILE__), 'data', 'test.jpg'), 'r') 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /test/test_pathname.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'helpers_for_test' 3 | require 'pathname' 4 | 5 | 6 | class TestPathname < TestCase 7 | 8 | include TempfileTest 9 | 10 | def setup 11 | super 12 | @org_filename = @data_dir + '/test.jpg' 13 | FileUtils.cp(@org_filename, @temp_filename) 14 | @temp_pathname = Pathname.new(@temp_filename) 15 | @mini_exiftool = MiniExiftool.new @temp_pathname 16 | end 17 | 18 | def test_pathname 19 | assert_equal 400, @mini_exiftool.iso 20 | @mini_exiftool.iso = 200 21 | assert_equal true, @mini_exiftool.save 22 | mini_exiftool = MiniExiftool.new(@temp_pathname) 23 | assert_equal 200, @mini_exiftool.iso 24 | @mini_exiftool.iso = 300 25 | assert_nothing_raised do 26 | @mini_exiftool.save! 27 | end 28 | assert_equal 300, @mini_exiftool.iso 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /test/test_pstore.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestPstore < TestCase 5 | 6 | def test_pstore 7 | pstore_dir = Dir.mktmpdir 8 | writable_tags = MiniExiftool.writable_tags 9 | res = execute(pstore_dir) 10 | t1 = res['time'] 11 | assert_equal writable_tags, res['writable_tags'] 12 | assert_equal 1, Dir[File.join(pstore_dir, '*')].size 13 | res = execute(pstore_dir) 14 | t2 = res['time'] 15 | assert_equal writable_tags, res['writable_tags'] 16 | assert t2 < 1.0, format('loading cached tag information should be done in under 1 second but needed %.2fs', t2) 17 | assert 10 * t2 < t1, format('loading cached tag information (%.2fs) should be 10 times faster than loading uncached information (%.2fs)', t2, t1) 18 | ensure 19 | FileUtils.rm_rf pstore_dir 20 | end 21 | 22 | private 23 | 24 | def execute pstore_dir 25 | script = <<-END 26 | MiniExiftool.pstore_dir = '#{pstore_dir}' 27 | res = {} 28 | start = Time.now 29 | res['writable_tags'] = MiniExiftool.writable_tags 30 | res['time'] = Time.now - start 31 | puts YAML.dump res 32 | END 33 | cmd = %Q(#{RUBY_ENGINE} -EUTF-8 -I lib -r mini_exiftool -r yaml -e "#{script}") 34 | YAML.unsafe_load(`#{cmd}`) 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /test/test_read.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestRead < TestCase 5 | 6 | def setup 7 | @data_dir = File.dirname(__FILE__) + '/data' 8 | @filename_test = @data_dir + '/test.jpg' 9 | @mini_exiftool = MiniExiftool.new @filename_test 10 | end 11 | 12 | def test_access 13 | assert_equal 'DYNAX 7D', @mini_exiftool['Model'] 14 | assert_equal 'MLT0', @mini_exiftool['maker_note_version'] 15 | assert_equal 'MLT0', @mini_exiftool[:MakerNoteVersion] 16 | assert_equal 'MLT0', @mini_exiftool[:maker_note_version] 17 | assert_equal 'MLT0', @mini_exiftool.maker_note_version 18 | assert_equal 400, @mini_exiftool.iso 19 | end 20 | 21 | def test_tags 22 | assert @mini_exiftool.tags.include?('FileSize') 23 | end 24 | 25 | def test_conversion 26 | assert_kind_of String, @mini_exiftool.model 27 | assert_kind_of Time, @mini_exiftool['DateTimeOriginal'] 28 | assert_kind_of Float, @mini_exiftool['MaxApertureValue'] 29 | assert_kind_of String, @mini_exiftool.flash 30 | assert_kind_of Integer, @mini_exiftool['ExposureCompensation'] 31 | assert_kind_of String, (@mini_exiftool['SubjectLocation'] || @mini_exiftool['SubjectArea']) 32 | assert_kind_of Array, @mini_exiftool['Keywords'] 33 | assert_kind_of String, @mini_exiftool['SupplementalCategories'] 34 | assert_kind_of Rational, @mini_exiftool.shutterspeed 35 | end 36 | 37 | def test_list_tags 38 | assert_equal ['Orange', 'Rot'], @mini_exiftool['Keywords'] 39 | assert_equal 'Natur', @mini_exiftool['SupplementalCategories'] 40 | assert_equal ['Natur'], Array(@mini_exiftool['SupplementalCategories']) 41 | end 42 | 43 | def test_value_encoding 44 | title= 'Abenddämmerung' 45 | assert_equal Encoding::UTF_8, @mini_exiftool.title.encoding 46 | assert_equal title, @mini_exiftool.title 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /test/test_read_coordinates.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestReadCoordinates < TestCase 5 | 6 | def setup 7 | @data_dir = File.dirname(__FILE__) + '/data' 8 | @filename_test = @data_dir + '/test_coordinates.jpg' 9 | end 10 | 11 | def test_access_coordinates 12 | mini_exiftool_coord = MiniExiftool.new @filename_test, :coord_format => "%.6f degrees" 13 | assert_match /^43.653167 degrees/, mini_exiftool_coord['GPSLatitude'] 14 | assert_match /^79.373167 degrees/, mini_exiftool_coord['GPSLongitude'] 15 | assert_match /^43.653167 degrees.*, 79.373167 degrees/, mini_exiftool_coord['GPSPosition'] 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /test/test_read_numerical.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestReadNumerical < TestCase 5 | 6 | def setup 7 | @data_dir = File.dirname(__FILE__) + '/data' 8 | @filename_test = @data_dir + '/test.jpg' 9 | @mini_exiftool_num = MiniExiftool.new @filename_test, :numerical => true 10 | end 11 | 12 | def test_access_numerical 13 | assert_equal 'DYNAX 7D', @mini_exiftool_num['Model'] 14 | assert_equal 'MLT0', @mini_exiftool_num['maker_note_version'] 15 | assert_equal 'MLT0', @mini_exiftool_num[:MakerNoteVersion] 16 | assert_equal 'MLT0', @mini_exiftool_num[:maker_note_version] 17 | assert_equal 'MLT0', @mini_exiftool_num.maker_note_version 18 | assert_equal 400, @mini_exiftool_num.iso 19 | end 20 | 21 | def test_conversion_numerical 22 | assert_kind_of String, @mini_exiftool_num.model 23 | assert_kind_of Time, @mini_exiftool_num['DateTimeOriginal'] 24 | assert_kind_of Float, @mini_exiftool_num['MaxApertureValue'] 25 | assert_kind_of Integer, @mini_exiftool_num.flash 26 | assert_kind_of String, @mini_exiftool_num.exif_version 27 | assert_kind_of Integer, @mini_exiftool_num['ExposureCompensation'] 28 | assert_kind_of String, (@mini_exiftool_num['SubjectLocation'] || @mini_exiftool_num['SubjectArea']) 29 | assert_kind_of Array, @mini_exiftool_num['Keywords'] 30 | assert_kind_of String, @mini_exiftool_num['SupplementalCategories'] 31 | end 32 | 33 | # Catching github issue [#40] 34 | # Values with leading zeros can have further zeros in it 35 | # Thanks to jvradelis 36 | def test_digits_starting_with_zero_and_has_further_zeros 37 | assert_equal '0300', @mini_exiftool_num['PrintIMVersion'] 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /test/test_save.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'digest/md5' 3 | require 'fileutils' 4 | require 'tempfile' 5 | require 'helpers_for_test' 6 | 7 | class TestSave < TestCase 8 | 9 | include TempfileTest 10 | 11 | def setup 12 | super 13 | @org_filename = @data_dir + '/test.jpg' 14 | FileUtils.cp(@org_filename, @temp_filename) 15 | @mini_exiftool = MiniExiftool.new @temp_filename 16 | @mini_exiftool_num = MiniExiftool.new @temp_filename, :numerical => true 17 | @org_md5 = Digest::MD5.hexdigest(File.read(@org_filename)) 18 | end 19 | 20 | def test_allowed_value 21 | @mini_exiftool_num['Orientation'] = 2 22 | result = @mini_exiftool_num.save 23 | assert_equal true, result 24 | assert_equal @org_md5, Digest::MD5.hexdigest(File.read(@org_filename)) 25 | assert_not_equal @org_md5, Digest::MD5.hexdigest(File.read(@temp_filename)) 26 | assert_equal false, @mini_exiftool_num.changed? 27 | result = @mini_exiftool_num.save 28 | assert_equal false, result 29 | end 30 | 31 | def test_non_allowed_value 32 | @mini_exiftool['Orientation'] = 'some string' 33 | result = @mini_exiftool.save 34 | assert_equal false, result 35 | assert_equal 1, @mini_exiftool.errors.size 36 | assert_match(/Can't convert IFD0:Orientation \(not in PrintConv\)/, 37 | @mini_exiftool.errors['Orientation']) 38 | assert @mini_exiftool.changed? 39 | assert @mini_exiftool.changed_tags.include?('Orientation') 40 | end 41 | 42 | def test_no_changing_of_file_when_error 43 | @mini_exiftool['ISO'] = 800 44 | @mini_exiftool['Orientation'] = 'some value' 45 | @mini_exiftool['ExposureTime'] = '1/30' 46 | result = @mini_exiftool.save 47 | assert_equal false, result 48 | assert_equal @org_md5, Digest::MD5.hexdigest(File.read(@org_filename)) 49 | assert_equal @org_md5, Digest::MD5.hexdigest(File.read(@temp_filename)) 50 | end 51 | 52 | def test_value_encoding 53 | special_string = 'äöü' 54 | @mini_exiftool.title = special_string 55 | assert @mini_exiftool.save 56 | @mini_exiftool.reload 57 | assert_equal Encoding::UTF_8, @mini_exiftool.title.encoding 58 | assert_equal special_string, @mini_exiftool.title 59 | end 60 | 61 | def test_save_bang 62 | @mini_exiftool.orientation = 'some value' 63 | exception = false 64 | begin 65 | @mini_exiftool.save! 66 | rescue MiniExiftool::Error => e 67 | assert_match /Orientation/, e.message 68 | exception = true 69 | end 70 | assert exception, "No exception when save! with error." 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /test/test_special.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestSpecial < TestCase 5 | 6 | include TempfileTest 7 | 8 | CAPTION_ABSTRACT = 'Some text for caption abstract' 9 | 10 | def setup 11 | super 12 | @org_filename = @data_dir + '/Canon.jpg' 13 | FileUtils.cp @org_filename, @temp_filename 14 | @canon = MiniExiftool.new @temp_filename 15 | end 16 | 17 | # Catching bug [#8073] 18 | # Thanks to Eric Young 19 | def test_special_chars 20 | assert_not_nil @canon['Self-timer'] 21 | assert_not_nil @canon.self_timer 22 | # preserving the original tag name 23 | assert @canon.tags.include?('Self-timer') || @canon.tags.include?('SelfTimer') 24 | assert !@canon.tags.include?('self_timer') 25 | end 26 | 27 | # Catching bug with writing caption-abstract 28 | # Thanks to Robin Romahn 29 | def test_caption_abstract_sensitive 30 | @canon['caption-abstract'] = CAPTION_ABSTRACT 31 | assert @canon.changed_tags.include?('Caption-Abstract') 32 | assert @canon.save 33 | assert_equal CAPTION_ABSTRACT, @canon.caption_abstract 34 | end 35 | 36 | def test_caption_abstract_non_sesitive 37 | @canon.caption_abstract = CAPTION_ABSTRACT.reverse 38 | assert @canon.save 39 | assert_equal CAPTION_ABSTRACT.reverse, @canon.caption_abstract 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /test/test_special_dates.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'date' 3 | require 'helpers_for_test' 4 | 5 | class TestSpecialDates < TestCase 6 | 7 | include TempfileTest 8 | 9 | def setup 10 | super 11 | @org_filename = @data_dir + '/test_special_dates.jpg' 12 | FileUtils.cp @org_filename, @temp_filename 13 | @mini_exiftool = MiniExiftool.new @temp_filename 14 | @mini_exiftool_datetime = MiniExiftool.new @temp_filename, 15 | :timestamps => DateTime 16 | end 17 | 18 | # Catching bug [#16328] (1st part) 19 | # Thanks to unknown 20 | def test_datetime 21 | datetime_original = @mini_exiftool.datetime_original 22 | if datetime_original 23 | assert_kind_of Time, datetime_original 24 | else 25 | assert_equal false, datetime_original 26 | end 27 | assert_kind_of DateTime, @mini_exiftool_datetime.datetime_original 28 | assert_raise MiniExiftool::Error do 29 | @mini_exiftool.timestamps = String 30 | @mini_exiftool.reload 31 | end 32 | @mini_exiftool.timestamps = DateTime 33 | @mini_exiftool.reload 34 | assert_equal @mini_exiftool_datetime.datetime_original, 35 | @mini_exiftool.datetime_original 36 | end 37 | 38 | # Catching bug [#16328] (2nd part) 39 | # Thanks to Cecil Coupe 40 | def test_invalid_date 41 | assert_equal false, @mini_exiftool.modify_date 42 | end 43 | 44 | def test_time_zone 45 | s = '1961-08-13 12:08:25+01:00' 46 | assert_equal Time.parse(s), @mini_exiftool.preview_date_time 47 | assert_equal DateTime.parse(s), 48 | @mini_exiftool_datetime.preview_date_time 49 | end 50 | 51 | end 52 | 53 | -------------------------------------------------------------------------------- /test/test_write.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'digest/md5' 3 | require 'fileutils' 4 | require 'tempfile' 5 | require 'helpers_for_test' 6 | 7 | class TestWrite < TestCase 8 | 9 | def setup 10 | @temp_file = Tempfile.new('test') 11 | @temp_file.close 12 | @temp_filename = @temp_file.path 13 | @org_filename = File.dirname(__FILE__) + '/data/test.jpg' 14 | FileUtils.cp(@org_filename, @temp_filename) 15 | @mini_exiftool = MiniExiftool.new @temp_filename 16 | @mini_exiftool_num = MiniExiftool.new @temp_filename, :numerical => true 17 | end 18 | 19 | def teardown 20 | @temp_file.delete 21 | end 22 | 23 | def test_access_existing_tags 24 | assert_equal 'Horizontal (normal)', @mini_exiftool['Orientation'] 25 | @mini_exiftool['Orientation'] = 'some string' 26 | assert_equal 'some string', @mini_exiftool['Orientation'] 27 | assert_equal false, @mini_exiftool.changed?('Orientation') 28 | @mini_exiftool['Orientation'] = 2 29 | assert_equal 2, @mini_exiftool['Orientation'] 30 | assert @mini_exiftool.changed_tags.include?('Orientation') 31 | @mini_exiftool.save 32 | assert_equal 'Mirror horizontal', @mini_exiftool['Orientation'] 33 | @mini_exiftool_num.reload 34 | assert_equal 2, @mini_exiftool_num['Orientation'] 35 | end 36 | 37 | def test_access_existing_tags_numerical 38 | assert_equal 1, @mini_exiftool_num['Orientation'] 39 | @mini_exiftool_num['Orientation'] = 2 40 | assert_equal 2, @mini_exiftool_num['Orientation'] 41 | assert_equal 2, @mini_exiftool_num.orientation 42 | @mini_exiftool_num.orientation = 3 43 | assert_equal 3, @mini_exiftool_num.orientation 44 | assert @mini_exiftool_num.changed_tags.include?('Orientation') 45 | @mini_exiftool_num.save 46 | assert_equal 3, @mini_exiftool_num['Orientation'] 47 | @mini_exiftool.reload 48 | assert_equal 'Rotate 180', @mini_exiftool['Orientation'] 49 | end 50 | 51 | def test_access_non_writable_tags 52 | @mini_exiftool_num['FileSize'] = 1 53 | assert_equal true, @mini_exiftool_num.changed? 54 | @mini_exiftool_num['SomeNonWritableName'] = 'test' 55 | assert_equal true, @mini_exiftool_num.changed? 56 | end 57 | 58 | # Catching rubyforge bug [#29596] 59 | # Thanks to Michael Grove for reporting 60 | # Part 1 61 | def test_quotes_in_values 62 | caption = "\"String in quotes\"" 63 | @mini_exiftool.caption = caption 64 | assert_equal true, @mini_exiftool.save, 'Saving error' 65 | @mini_exiftool.reload 66 | assert_equal caption, @mini_exiftool.caption 67 | end 68 | 69 | # Catching rubyforge bug [#29596] 70 | # Thanks to Michael Grove for reporting 71 | # Part 2 72 | def test_quotes_and_apostrophe_in_values 73 | caption = caption = "\"Watch your step, it's slippery.\"" 74 | @mini_exiftool.caption = caption 75 | assert_equal true, @mini_exiftool.save, 'Saving error' 76 | @mini_exiftool.reload 77 | assert_equal caption, @mini_exiftool.caption 78 | end 79 | 80 | def test_time_conversion 81 | t = Time.now 82 | @mini_exiftool_num['DateTimeOriginal'] = t 83 | assert_kind_of Time, @mini_exiftool_num['DateTimeOriginal'] 84 | assert_equal true, @mini_exiftool_num.changed_tags.include?('DateTimeOriginal') 85 | @mini_exiftool_num.save 86 | assert_equal false, @mini_exiftool_num.changed? 87 | assert_kind_of Time, @mini_exiftool_num['DateTimeOriginal'] 88 | assert_equal t.to_s, @mini_exiftool_num['DateTimeOriginal'].to_s 89 | end 90 | 91 | def test_float_conversion 92 | assert_kind_of Float, @mini_exiftool_num['BrightnessValue'] 93 | new_time = @mini_exiftool_num['BrightnessValue'] + 1 94 | @mini_exiftool_num['BrightnessValue'] = new_time 95 | assert_equal new_time, @mini_exiftool_num['BrightnessValue'] 96 | assert_equal true, @mini_exiftool_num.changed_tags.include?('BrightnessValue') 97 | @mini_exiftool_num.save 98 | assert_kind_of Float, @mini_exiftool_num['BrightnessValue'] 99 | assert_equal new_time, @mini_exiftool_num['BrightnessValue'] 100 | end 101 | 102 | def test_integer_conversion 103 | assert_kind_of Integer, @mini_exiftool_num['MeteringMode'] 104 | new_mode = @mini_exiftool_num['MeteringMode'] - 1 105 | @mini_exiftool_num['MeteringMode'] = new_mode 106 | assert_equal new_mode, @mini_exiftool_num['MeteringMode'] 107 | assert @mini_exiftool_num.changed_tags.include?('MeteringMode') 108 | @mini_exiftool_num.save 109 | assert_equal new_mode, @mini_exiftool_num['MeteringMode'] 110 | end 111 | 112 | def test_rational_conversion 113 | new_exposure_time = Rational(1, 125) 114 | @mini_exiftool.exposure_time = new_exposure_time 115 | assert @mini_exiftool.changed?, 'No changing of value.' 116 | ok = @mini_exiftool.save 117 | assert ok, 'Saving failed.' 118 | @mini_exiftool.reload 119 | assert_equal new_exposure_time, @mini_exiftool.exposure_time 120 | end 121 | 122 | def test_list_conversion 123 | arr = ['a', 'b', 'c'] 124 | @mini_exiftool['Keywords'] = arr 125 | ok = @mini_exiftool.save 126 | assert ok 127 | assert_equal arr, @mini_exiftool['Keywords'] 128 | arr = ['text, with', 'commas, let us look'] 129 | @mini_exiftool['Keywords'] = arr 130 | ok = @mini_exiftool.save 131 | assert ok 132 | if MiniExiftool.exiftool_version.to_f < 7.41 133 | assert_equal ['text', 'with', 'commas', 'let us look'], @mini_exiftool['Keywords'] 134 | else 135 | assert_equal arr, @mini_exiftool['Keywords'] 136 | end 137 | end 138 | 139 | def test_revert_one 140 | @mini_exiftool_num['Orientation'] = 2 141 | @mini_exiftool_num['ISO'] = 200 142 | res = @mini_exiftool_num.revert 'Orientation' 143 | assert_equal 1, @mini_exiftool_num['Orientation'] 144 | assert_equal 200, @mini_exiftool_num['ISO'] 145 | assert_equal true, res 146 | res = @mini_exiftool_num.revert 'Orientation' 147 | assert_equal false, res 148 | end 149 | 150 | def test_revert_all 151 | @mini_exiftool_num['Orientation'] = 2 152 | @mini_exiftool_num['ISO'] = 200 153 | res = @mini_exiftool_num.revert 154 | assert_equal 1, @mini_exiftool_num['Orientation'] 155 | assert_equal 400, @mini_exiftool_num['ISO'] 156 | assert_equal true, res 157 | res = @mini_exiftool_num.revert 158 | assert_equal false, res 159 | end 160 | 161 | end 162 | --------------------------------------------------------------------------------