├── .vscode └── settings.json ├── AUTHORS ├── LICENSE ├── README.md ├── _locales ├── de │ └── messages.json ├── en │ └── messages.json ├── ru │ └── messages.json └── uk │ └── messages.json ├── backgroundscript.js ├── binExif.js ├── binIptc.js ├── contentscript.js ├── fxifUtils.js ├── icons └── fxif.png ├── manifest.json ├── parseJpeg.js ├── popup ├── popup.css ├── popup.html └── popupscript.js ├── stringBundle.js └── xmp.js /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.tabSize": 2 4 | } -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Matthias Wandel (JHead developer) 2 | Chris Beaven (FxIF icon) 3 | Ted Mielczarek 4 | Christian Eyrich 5 | Gian-Carlo Pascutto 6 | Martin Arndt 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wxIF 2 | 3 | wxIF is a port of FxIF to WebExtensions. It adds a context menu that 4 | allows one to view EXIF, IPTC and XMP metadata from images. 5 | 6 | ## Differences with the XUL version 7 | 8 | * It's not possible to verify that the image can potentially contain 9 | EXIF data without downloading it. So the context menu item will be 10 | displayed even on e.g. PNG images. 11 | 12 | * Images are redownloaded instead of being fetched directly from the 13 | cache. (The re-download may still be statisfied from cache). 14 | 15 | ## To Do 16 | 17 | * The info panel layout is rather clunky. It should probably size to 18 | its contents somehow. 19 | 20 | * IPTC and XMP metadata support hasn't had much if any testing. 21 | 22 | * The i18n support is a quick hack to be able to use the messages 23 | from the previous version. This will need an overhaul. 24 | 25 | * No translations from the original addon were ported. 26 | 27 | * The coordinate notation and map provider are not configurable. 28 | 29 | ## License 30 | 31 | MPL 2.0 32 | -------------------------------------------------------------------------------- /_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionDescription": { 3 | "message": "Zeigt EXIF- / IPTC- / XMP-Metadaten an.", 4 | "description": "WebExtensions-Portierung von FxIF, um EXIF-Informationen von JPEG-Dateien anzuzeigen." 5 | }, 6 | 7 | "contextMenuText": { 8 | "message": "EXIF-Daten" 9 | }, 10 | 11 | "Copy": { 12 | "message": "Kopieren" 13 | }, 14 | 15 | "CameraMake": { 16 | "message": "Hersteller" 17 | }, 18 | 19 | "CameraModel": { 20 | "message": "Modell" 21 | }, 22 | 23 | "CameraLens": { 24 | "message": "Objektiv" 25 | }, 26 | 27 | "Software": { 28 | "message": "Programm" 29 | }, 30 | 31 | "ImageDate": { 32 | "message": "Aufnahmedatum" 33 | }, 34 | 35 | "ImageOrientation": { 36 | "message": "Ausrichtung" 37 | }, 38 | 39 | "ImageBW": { 40 | "message": "schwarz-weiß" 41 | }, 42 | 43 | "FlashUsed": { 44 | "message": "Blitz aktiv" 45 | }, 46 | 47 | "FocalLength": { 48 | "message": "Brennweite" 49 | }, 50 | 51 | "DigitalZoom": { 52 | "message": "Digital-Zoom" 53 | }, 54 | 55 | "CCDWidth": { 56 | "message": "CCD-Weite" 57 | }, 58 | 59 | "ExposureTime": { 60 | "message": "Belichtungsdauer" 61 | }, 62 | 63 | "Aperture": { 64 | "message": "Blende" 65 | }, 66 | 67 | "FocusDist": { 68 | "message": "Fokus-Abstand" 69 | }, 70 | 71 | "ISOequivalent": { 72 | "message": "ISO-Äquivalent" 73 | }, 74 | 75 | "ExposureBias": { 76 | "message": "Belichtungsausrichtung" 77 | }, 78 | 79 | "WhiteBalance": { 80 | "message": "Weißabgleich" 81 | }, 82 | 83 | "LightSource": { 84 | "message": "Lichtquelle" 85 | }, 86 | 87 | "MeteringMode": { 88 | "message": "Messart" 89 | }, 90 | 91 | "ExposureProgram": { 92 | "message": "Belichtung" 93 | }, 94 | 95 | "ExposureMode": { 96 | "message": "Belichtungsart" 97 | }, 98 | 99 | "ImageColorSpace": { 100 | "message": "Farbraum" 101 | }, 102 | 103 | "GPSCoord": { 104 | "message": "GPS-Koordinaten" 105 | }, 106 | 107 | "MapLink": { 108 | "message": "Karten-Link" 109 | }, 110 | 111 | "GPSAlt": { 112 | "message": "GPS-Höhe" 113 | }, 114 | 115 | "GPSImgDir": { 116 | "message": "GPS-Blickrichtung" 117 | }, 118 | 119 | "Creator": { 120 | "message": "Urheber" 121 | }, 122 | 123 | "City": { 124 | "message": "Stadt" 125 | }, 126 | 127 | "Sublocation": { 128 | "message": "Region" 129 | }, 130 | 131 | "ProvinceState": { 132 | "message": "Bundesland/Gebiet" 133 | }, 134 | 135 | "CountryName": { 136 | "message": "Land" 137 | }, 138 | 139 | "Copyright": { 140 | "message": "©" 141 | }, 142 | 143 | "Title": { 144 | "message": "Titel" 145 | }, 146 | 147 | "Caption": { 148 | "message": "Beschreibung" 149 | }, 150 | 151 | "Comment": { 152 | "message": "Kommentar" 153 | }, 154 | 155 | "Instructions": { 156 | "message": "Anweisungen" 157 | }, 158 | 159 | "NoData": { 160 | "message": "Keine Daten vorhanden" 161 | }, 162 | 163 | "GPSFormat": { 164 | "message": "Koordinatenformat" 165 | }, 166 | 167 | "DD.label": { 168 | "message": "° (dezimal)" 169 | }, 170 | 171 | "DM.label": { 172 | "message": "° (Minuten)" 173 | }, 174 | 175 | "DMS.label": { 176 | "message": "° (Minuten/Sekunden)" 177 | }, 178 | 179 | "windowtitle": { 180 | "message": "wxIF-Daten für" 181 | }, 182 | 183 | "orientation0": { 184 | "message": "undefiniert" 185 | }, 186 | 187 | "orientation1": { 188 | "message": "normal" 189 | }, 190 | 191 | "orientation2": { 192 | "message": "horizontal gespiegelt" 193 | }, 194 | 195 | "orientation3": { 196 | "message": "180° gedreht" 197 | }, 198 | 199 | "orientation4": { 200 | "message": "vertikal gespiegelt" 201 | }, 202 | 203 | "orientation5": { 204 | "message": "diagonal gespiegelt" 205 | }, 206 | 207 | "orientation6": { 208 | "message": "90° gedreht" 209 | }, 210 | 211 | "orientation7": { 212 | "message": "umgekehrt diagonal gespiegelt" 213 | }, 214 | 215 | "orientation8": { 216 | "message": "270° gedreht" 217 | }, 218 | 219 | "infinite": { 220 | "message": "unendlich" 221 | }, 222 | 223 | "meters": { 224 | "message": "m" 225 | }, 226 | 227 | "millimeters": { 228 | "message": "mm" 229 | }, 230 | 231 | "seconds": { 232 | "message": "s" 233 | }, 234 | 235 | "ev": { 236 | "message": "BW" 237 | }, 238 | 239 | "yes": { 240 | "message": "Ja" 241 | }, 242 | 243 | "no": { 244 | "message": "Nein" 245 | }, 246 | 247 | "enforced": { 248 | "message": "erzwungen" 249 | }, 250 | 251 | "manual": { 252 | "message": "manuell" 253 | }, 254 | 255 | "auto": { 256 | "message": "automatisch" 257 | }, 258 | 259 | "semiauto": { 260 | "message": "halbautomatisch" 261 | }, 262 | 263 | "none": { 264 | "message": "kein" 265 | }, 266 | 267 | "daylight": { 268 | "message": "Tageslicht" 269 | }, 270 | 271 | "fluorescent": { 272 | "message": "Neonlicht" 273 | }, 274 | 275 | "incandescent": { 276 | "message": "Glühlampe" 277 | }, 278 | 279 | "flash": { 280 | "message": "Blitz" 281 | }, 282 | 283 | "fineweather": { 284 | "message": "Schönwetter" 285 | }, 286 | 287 | "cloudy": { 288 | "message": "Bewölkt" 289 | }, 290 | 291 | "shade": { 292 | "message": "Schatten" 293 | }, 294 | 295 | "daylightfluorescent": { 296 | "message": "Tageslicht" 297 | }, 298 | 299 | "daywhitefluorescent": { 300 | "message": "warmes Weißlicht" 301 | }, 302 | 303 | "coolwhitefluorescent": { 304 | "message": "kaltes Weißlicht" 305 | }, 306 | 307 | "whitefluorescent": { 308 | "message": "Weißlicht" 309 | }, 310 | 311 | "studiotungsten": { 312 | "message": "ISO-Studio (Wolfram)" 313 | }, 314 | 315 | "noflash": { 316 | "message": "Blitz nicht verfügbar" 317 | }, 318 | 319 | "noreturnlight": { 320 | "message": "return light not detected" 321 | }, 322 | 323 | "returnlight": { 324 | "message": "return light detected" 325 | }, 326 | 327 | "redeye": { 328 | "message": "Rote-Augen-Reduktion" 329 | }, 330 | 331 | "average": { 332 | "message": "Durchschnitt" 333 | }, 334 | 335 | "centerweight": { 336 | "message": "mittenbasiert" 337 | }, 338 | 339 | "spot": { 340 | "message": "Punkt" 341 | }, 342 | 343 | "multispot": { 344 | "message": "Mehrpunkt" 345 | }, 346 | 347 | "matrix": { 348 | "message": "Matrix" 349 | }, 350 | 351 | "partial": { 352 | "message": "anteilig" 353 | }, 354 | 355 | "autobracketing": { 356 | "message": "automatische Belichtungsreihe" 357 | }, 358 | 359 | "35mmequiv": { 360 | "message": "mm=35mm-Äquivalent" 361 | }, 362 | 363 | "program": { 364 | "message": "Programm" 365 | }, 366 | 367 | "apriority": { 368 | "message": "Blenden-Priorität" 369 | }, 370 | 371 | "spriority": { 372 | "message": "Verschluss-Priorität" 373 | }, 374 | 375 | "creative": { 376 | "message": "Kreativ-Programm (für Tiefenschärfe)" 377 | }, 378 | 379 | "action": { 380 | "message": "Action-Programm (für schnelle Verschlusszeit)" 381 | }, 382 | 383 | "portrait": { 384 | "message": "Hochformat" 385 | }, 386 | 387 | "landscape": { 388 | "message": "Querformat" 389 | }, 390 | 391 | "latlondms": { 392 | "message": " (°, min, s)" 393 | }, 394 | 395 | "latlondm": { 396 | "message": " (°, min)" 397 | }, 398 | 399 | "latlondd": { 400 | "message": " (°)" 401 | }, 402 | 403 | "dirT": { 404 | "message": "° (echter Norden)" 405 | }, 406 | 407 | "dirM": { 408 | "message": "° (magnetischer Norden)" 409 | }, 410 | 411 | "noTZ": { 412 | "message": "(Keine Zeitzone)" 413 | }, 414 | 415 | "unknown": { 416 | "message": "unbekannt" 417 | }, 418 | 419 | "generalError": { 420 | "message": "Fehler bei der Metadaten-Verarbeitung: angezeigte Daten ggf. unvollständig. Bitte mit Beispielbild als Fehler melden." 421 | }, 422 | 423 | "specialError": { 424 | "message": "Fehler bei der %S-Verarbeitung. angezeigte Daten ggf. unvollständig. Bitte mit Beispielbild als Fehler melden." 425 | }, 426 | 427 | "UserComment": { 428 | "message": "Nutzerkommentar" 429 | }, 430 | 431 | "ColorSpace": { 432 | "message": "Farbraum" 433 | }, 434 | 435 | "FocalLengthText": { 436 | "message": "Brennweite" 437 | }, 438 | 439 | "ApertureFNumber": { 440 | "message": "Blendenzahl" 441 | }, 442 | 443 | "FocalLength35mmEquiv": { 444 | "message": "Brennweite (35mm)" 445 | }, 446 | 447 | "GPSLat": { 448 | "message": "GPS-Breitengrad" 449 | }, 450 | 451 | "GPSLon": { 452 | "message": "GPS-Längengrad" 453 | }, 454 | 455 | "noEXIFdata": { 456 | "message": "Keine Bild-Metadaten vorhanden" 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionDescription": { 3 | "message": "View the EXIF/IPTC/XMP data for images.", 4 | "description": "Description of the extension." 5 | }, 6 | 7 | "contextMenuText": { 8 | "message": "View EXIF data" 9 | }, 10 | 11 | "Copy": { 12 | "message": "Copy" 13 | }, 14 | 15 | "CameraMake": { 16 | "message": "Camera Maker" 17 | }, 18 | 19 | "CameraModel": { 20 | "message": "Camera Model" 21 | }, 22 | 23 | "CameraLens": { 24 | "message": "Lens" 25 | }, 26 | 27 | "Software": { 28 | "message": "Software" 29 | }, 30 | 31 | "ImageDate": { 32 | "message": "Image Date" 33 | }, 34 | 35 | "ImageOrientation": { 36 | "message": "Orientation" 37 | }, 38 | 39 | "ImageBW": { 40 | "message": "Black and White" 41 | }, 42 | 43 | "FlashUsed": { 44 | "message": "Flash Fired" 45 | }, 46 | 47 | "FocalLength": { 48 | "message": "Focal Length" 49 | }, 50 | 51 | "DigitalZoom": { 52 | "message": "Digital Zoom" 53 | }, 54 | 55 | "CCDWidth": { 56 | "message": "CCD Width" 57 | }, 58 | 59 | "ExposureTime": { 60 | "message": "Exposure Time" 61 | }, 62 | 63 | "Aperture": { 64 | "message": "Aperture" 65 | }, 66 | 67 | "FocusDist": { 68 | "message": "Focus Distance" 69 | }, 70 | 71 | "ISOequivalent": { 72 | "message": "ISO equivalent" 73 | }, 74 | 75 | "ExposureBias": { 76 | "message": "Exposure Bias" 77 | }, 78 | 79 | "WhiteBalance": { 80 | "message": "White Balance" 81 | }, 82 | 83 | "LightSource": { 84 | "message": "Light Source" 85 | }, 86 | 87 | "MeteringMode": { 88 | "message": "Metering Mode" 89 | }, 90 | 91 | "ExposureProgram": { 92 | "message": "Exposure" 93 | }, 94 | 95 | "ExposureMode": { 96 | "message": "Exposure Mode" 97 | }, 98 | 99 | "ImageColorSpace": { 100 | "message": "Color Space" 101 | }, 102 | 103 | "GPSCoord": { 104 | "message": "GPS Coordinate" 105 | }, 106 | 107 | "MapLink": { 108 | "message": "Map Link" 109 | }, 110 | 111 | "GPSAlt": { 112 | "message": "GPS Altitude" 113 | }, 114 | 115 | "GPSImgDir": { 116 | "message": "Viewing Direction" 117 | }, 118 | 119 | "Creator": { 120 | "message": "Creator" 121 | }, 122 | 123 | "City": { 124 | "message": "City" 125 | }, 126 | 127 | "Sublocation": { 128 | "message": "Sublocation" 129 | }, 130 | 131 | "ProvinceState": { 132 | "message": "Province/State" 133 | }, 134 | 135 | "CountryName": { 136 | "message": "Country" 137 | }, 138 | 139 | "Copyright": { 140 | "message": "Copyright" 141 | }, 142 | 143 | "Title": { 144 | "message": "Title" 145 | }, 146 | 147 | "Caption": { 148 | "message": "Caption" 149 | }, 150 | 151 | "Comment": { 152 | "message": "Comment" 153 | }, 154 | 155 | "Instructions": { 156 | "message": "Instructions" 157 | }, 158 | 159 | "NoData": { 160 | "message": "No meta data found" 161 | }, 162 | 163 | "GPSFormat": { 164 | "message": "Coordinates format to use" 165 | }, 166 | 167 | "DD.label": { 168 | "message": "Decimal degree" 169 | }, 170 | 171 | "DM.label": { 172 | "message": "Degree Minute" 173 | }, 174 | 175 | "DMS.label": { 176 | "message": "Degree Minutes Second" 177 | }, 178 | 179 | "windowtitle": { 180 | "message": "FxIF Data for" 181 | }, 182 | 183 | "orientation0": { 184 | "message": "Undefined" 185 | }, 186 | 187 | "orientation1": { 188 | "message": "Normal" 189 | }, 190 | 191 | "orientation2": { 192 | "message": "Flip Horizontal" 193 | }, 194 | 195 | "orientation3": { 196 | "message": "Rotate 180" 197 | }, 198 | 199 | "orientation4": { 200 | "message": "Flip Vertical" 201 | }, 202 | 203 | "orientation5": { 204 | "message": "Transpose" 205 | }, 206 | 207 | "orientation6": { 208 | "message": "Rotate 90" 209 | }, 210 | 211 | "orientation7": { 212 | "message": "Transverse" 213 | }, 214 | 215 | "orientation8": { 216 | "message": "Rotate 270" 217 | }, 218 | 219 | "infinite": { 220 | "message": "Infinite" 221 | }, 222 | 223 | "meters": { 224 | "message": "m" 225 | }, 226 | 227 | "millimeters": { 228 | "message": "mm" 229 | }, 230 | 231 | "seconds": { 232 | "message": "s" 233 | }, 234 | 235 | "ev": { 236 | "message": "EV" 237 | }, 238 | 239 | "yes": { 240 | "message": "Yes" 241 | }, 242 | 243 | "no": { 244 | "message": "No" 245 | }, 246 | 247 | "enforced": { 248 | "message": "enforced" 249 | }, 250 | 251 | "manual": { 252 | "message": "Manual" 253 | }, 254 | 255 | "auto": { 256 | "message": "Auto" 257 | }, 258 | 259 | "semiauto": { 260 | "message": "semi-auto" 261 | }, 262 | 263 | "none": { 264 | "message": "none" 265 | }, 266 | 267 | "daylight": { 268 | "message": "Daylight" 269 | }, 270 | 271 | "fluorescent": { 272 | "message": "Fluorescent" 273 | }, 274 | 275 | "incandescent": { 276 | "message": "Incandescent" 277 | }, 278 | 279 | "flash": { 280 | "message": "Flash" 281 | }, 282 | 283 | "fineweather": { 284 | "message": "Fine Weather" 285 | }, 286 | 287 | "cloudy": { 288 | "message": "Cloudy" 289 | }, 290 | 291 | "shade": { 292 | "message": "Shade" 293 | }, 294 | 295 | "daylightfluorescent": { 296 | "message": "Daylight Fluorescent" 297 | }, 298 | 299 | "daywhitefluorescent": { 300 | "message": "Day White Fluorescent" 301 | }, 302 | 303 | "coolwhitefluorescent": { 304 | "message": "Cool White Fluorescent" 305 | }, 306 | 307 | "whitefluorescent": { 308 | "message": "White Fluorescent" 309 | }, 310 | 311 | "studiotungsten": { 312 | "message": "ISO Studio Tungsten" 313 | }, 314 | 315 | "noflash": { 316 | "message": "No Flash available" 317 | }, 318 | 319 | "noreturnlight": { 320 | "message": "return light not detected" 321 | }, 322 | 323 | "returnlight": { 324 | "message": "return light detected" 325 | }, 326 | 327 | "redeye": { 328 | "message": "red eye reduction mode" 329 | }, 330 | 331 | "average": { 332 | "message": "Average" 333 | }, 334 | 335 | "centerweight": { 336 | "message": "Center Weight" 337 | }, 338 | 339 | "spot": { 340 | "message": "Spot" 341 | }, 342 | 343 | "multispot": { 344 | "message": "Multispot" 345 | }, 346 | 347 | "matrix": { 348 | "message": "Matrix" 349 | }, 350 | 351 | "partial": { 352 | "message": "Partial" 353 | }, 354 | 355 | "autobracketing": { 356 | "message": "Auto Bracketing" 357 | }, 358 | 359 | "35mmequiv": { 360 | "message": "mm=35mm equivalent" 361 | }, 362 | 363 | "program": { 364 | "message": "program" 365 | }, 366 | 367 | "apriority": { 368 | "message": "aperture priority" 369 | }, 370 | 371 | "spriority": { 372 | "message": "shutter priority" 373 | }, 374 | 375 | "creative": { 376 | "message": "Creative Program (based towards depth of field)" 377 | }, 378 | 379 | "action": { 380 | "message": "Action program (based towards fast shutter speed)" 381 | }, 382 | 383 | "portrait": { 384 | "message": "Portrait Mode" 385 | }, 386 | 387 | "landscape": { 388 | "message": "Landscape Mode" 389 | }, 390 | 391 | "latlondms": { 392 | "message": " (deg, min, sec)" 393 | }, 394 | 395 | "latlondm": { 396 | "message": " (deg, min)" 397 | }, 398 | 399 | "latlondd": { 400 | "message": " (deg )" 401 | }, 402 | 403 | "dirT": { 404 | "message": "° (true north)" 405 | }, 406 | 407 | "dirM": { 408 | "message": "° (magnetic north)" 409 | }, 410 | 411 | "noTZ": { 412 | "message": "(no TZ)" 413 | }, 414 | 415 | "unknown": { 416 | "message": "Unknown" 417 | }, 418 | 419 | "generalError": { 420 | "message": "Error while interpreting the meta data. Meta data shown might be not complete. Please report this problem with an example image." 421 | }, 422 | 423 | "specialError": { 424 | "message": "Error while interpreting %S data. %S meta data shown might be not complete. Please report this problem with an example image." 425 | }, 426 | 427 | "UserComment": { 428 | "message": "User Comment" 429 | }, 430 | 431 | "ColorSpace": { 432 | "message": "Color Space" 433 | }, 434 | 435 | "FocalLengthText": { 436 | "message": "Focal Length" 437 | }, 438 | 439 | "ApertureFNumber": { 440 | "message": "Aperture" 441 | }, 442 | 443 | "FocalLength35mmEquiv": { 444 | "message": "Focal Length (35mm)" 445 | }, 446 | 447 | "GPSLat": { 448 | "message": "GPS Latitude" 449 | }, 450 | 451 | "GPSLon": { 452 | "message": "GPS Longittude" 453 | }, 454 | 455 | "noEXIFdata": { 456 | "message": "No image metadata found" 457 | } 458 | } -------------------------------------------------------------------------------- /_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionDescription": { 3 | "message": "Ищет и показывает EXIF/IPTC/XMP-данные фотографий.", 4 | "description": "Description of the extension." 5 | }, 6 | 7 | "contextMenuText": { 8 | "message": "Просмотр EXIF-данных" 9 | }, 10 | 11 | "Copy": { 12 | "message": "Копировать" 13 | }, 14 | 15 | "CameraMake": { 16 | "message": "Производитель фотоаппарата" 17 | }, 18 | 19 | "CameraModel": { 20 | "message": "Модель фотоаппарата" 21 | }, 22 | 23 | "CameraLens": { 24 | "message": "Объектив" 25 | }, 26 | 27 | "Software": { 28 | "message": "Программное Обеспечение" 29 | }, 30 | 31 | "ImageDate": { 32 | "message": "Дата съёмки" 33 | }, 34 | 35 | "ImageOrientation": { 36 | "message": "Ориентация" 37 | }, 38 | 39 | "ImageBW": { 40 | "message": "Черно-Белое" 41 | }, 42 | 43 | "FlashUsed": { 44 | "message": "Вспышка сработала" 45 | }, 46 | 47 | "FocalLength": { 48 | "message": "Фокусное расстояние" 49 | }, 50 | 51 | "DigitalZoom": { 52 | "message": "Цифровой зум" 53 | }, 54 | 55 | "CCDWidth": { 56 | "message": "Ширина матрицы (CCD/ПЗС)" 57 | }, 58 | 59 | "ExposureTime": { 60 | "message": "Длительность экспозиции (выдержка)" 61 | }, 62 | 63 | "Aperture": { 64 | "message": "Диафрагма" 65 | }, 66 | 67 | "FocusDist": { 68 | "message": "Фокусное расстояние" 69 | }, 70 | 71 | "ISOequivalent": { 72 | "message": "ISO-эквивалент" 73 | }, 74 | 75 | "ExposureBias": { 76 | "message": "Экспокоррекция" 77 | }, 78 | 79 | "WhiteBalance": { 80 | "message": "Баланс белого" 81 | }, 82 | 83 | "LightSource": { 84 | "message": "Источник света" 85 | }, 86 | 87 | "MeteringMode": { 88 | "message": "Режим Экспозамера" 89 | }, 90 | 91 | "ExposureProgram": { 92 | "message": "Экспозиция" 93 | }, 94 | 95 | "ExposureMode": { 96 | "message": "Режим экспозиции" 97 | }, 98 | 99 | "ImageColorSpace": { 100 | "message": "Цветовое пространство" 101 | }, 102 | 103 | "GPSCoord": { 104 | "message": "GPS-координаты" 105 | }, 106 | 107 | "MapLink": { 108 | "message": "Ссылка на карту" 109 | }, 110 | 111 | "GPSAlt": { 112 | "message": "GPS-Высота" 113 | }, 114 | 115 | "GPSImgDir": { 116 | "message": "Направление взгляда" 117 | }, 118 | 119 | "Creator": { 120 | "message": "Автор" 121 | }, 122 | 123 | "City": { 124 | "message": "Город" 125 | }, 126 | 127 | "Район": { 128 | "message": "Район" 129 | }, 130 | 131 | "ProvinceState": { 132 | "message": "Провинция/Штат" 133 | }, 134 | 135 | "CountryName": { 136 | "message": "Страна" 137 | }, 138 | 139 | "Copyright": { 140 | "message": "Авторское право" 141 | }, 142 | 143 | "Title": { 144 | "message": "Название" 145 | }, 146 | 147 | "Caption": { 148 | "message": "Заголовок" 149 | }, 150 | 151 | "Comment": { 152 | "message": "Комментарий" 153 | }, 154 | 155 | "Instructions": { 156 | "message": "Инструкции" 157 | }, 158 | 159 | "NoData": { 160 | "message": "Мета-данные не найдены" 161 | }, 162 | 163 | "GPSFormat": { 164 | "message": "Желаемый формат отображения координат" 165 | }, 166 | 167 | "DD.label": { 168 | "message": "Десятичные градусы" 169 | }, 170 | 171 | "DM.label": { 172 | "message": "Градусы, Минуты" 173 | }, 174 | 175 | "DMS.label": { 176 | "message": "Градусы, Минуты, Секунды" 177 | }, 178 | 179 | "windowtitle": { 180 | "message": "FxIF Данные для" 181 | }, 182 | 183 | "orientation0": { 184 | "message": "Не определено" 185 | }, 186 | 187 | "orientation1": { 188 | "message": "Нормальная" 189 | }, 190 | 191 | "orientation2": { 192 | "message": "Отражено по горизонтали" 193 | }, 194 | 195 | "orientation3": { 196 | "message": "Поворот на 180" 197 | }, 198 | 199 | "orientation4": { 200 | "message": "Отражено по вертикали" 201 | }, 202 | 203 | "orientation5": { 204 | "message": "Transpose (отражение+поворот на 90) - отражено по оси, проходящей через верхний левый и нижний правый углы квадрата." 205 | }, 206 | 207 | "orientation6": { 208 | "message": "Поворот на 90" 209 | }, 210 | 211 | "orientation7": { 212 | "message": "Transverse (отражение+поворот на 90) - отражено по оси, проходящей через верхний правый и нижний левый углы квадрата." 213 | }, 214 | 215 | "orientation8": { 216 | "message": "Оборот на 270" 217 | }, 218 | 219 | "infinite": { 220 | "message": "Фокус на бесконечность" 221 | }, 222 | 223 | "meters": { 224 | "message": "м" 225 | }, 226 | 227 | "millimeters": { 228 | "message": "мм" 229 | }, 230 | 231 | "seconds": { 232 | "message": "с" 233 | }, 234 | 235 | "ev": { 236 | "message": "Экспозиционное число (EV)" 237 | }, 238 | 239 | "yes": { 240 | "message": "Да" 241 | }, 242 | 243 | "no": { 244 | "message": "Нет" 245 | }, 246 | 247 | "enforced": { 248 | "message": "Принудительно" 249 | }, 250 | 251 | "manual": { 252 | "message": "Ручной" 253 | }, 254 | 255 | "auto": { 256 | "message": "Автоматический" 257 | }, 258 | 259 | "semiauto": { 260 | "message": "Полу-автоматический" 261 | }, 262 | 263 | "none": { 264 | "message": "Пресеты ББ не использованы" 265 | }, 266 | 267 | "daylight": { 268 | "message": "Дневной свет" 269 | }, 270 | 271 | "fluorescent": { 272 | "message": "Люминесцентная лампа" 273 | }, 274 | 275 | "incandescent": { 276 | "message": "Лампа накаливания" 277 | }, 278 | 279 | "flash": { 280 | "message": "Со вспышкой" 281 | }, 282 | 283 | "fineweather": { 284 | "message": "Ясная погода" 285 | }, 286 | 287 | "cloudy": { 288 | "message": "Облачно" 289 | }, 290 | 291 | "shade": { 292 | "message": "Тень" 293 | }, 294 | 295 | "daylightfluorescent": { 296 | "message": "Люминесцентная лампа дневного света" 297 | }, 298 | 299 | "daywhitefluorescent": { 300 | "message": "Люминесцентная лампа дневного белого света" 301 | }, 302 | 303 | "coolwhitefluorescent": { 304 | "message": "Люминесцентная лампа холодно-белого света" 305 | }, 306 | 307 | "whitefluorescent": { 308 | "message": "Люминесцентная лампа белого света" 309 | }, 310 | 311 | "studiotungsten": { 312 | "message": "ISO для студийных ламп накаливания" 313 | }, 314 | 315 | "noflash": { 316 | "message": "Без вспышки" 317 | }, 318 | 319 | "noreturnlight": { 320 | "message": "Отраженный свет вспышки не обнаружен датчиком - система не определила уровень мощности вспышки на основе предварительной вспышки, в режиме TTL" 321 | }, 322 | 323 | "returnlight": { 324 | "message": "Отраженный свет вспышки обнаружен датчиком - система определила уровень мощности вспышки на основе предварительной вспышки, в режиме TTL" 325 | }, 326 | 327 | "redeye": { 328 | "message": "Режим уменьшения эффекта красных глаз" 329 | }, 330 | 331 | "average": { 332 | "message": "Обобщенный" 333 | }, 334 | 335 | "centerweight": { 336 | "message": "Центро-Взвешенный" 337 | }, 338 | 339 | "spot": { 340 | "message": "Точечный" 341 | }, 342 | 343 | "multispot": { 344 | "message": "Много-Точечный" 345 | }, 346 | 347 | "matrix": { 348 | "message": "Матричный (оценочный, многозонный)" 349 | }, 350 | 351 | "partial": { 352 | "message": "Частичный" 353 | }, 354 | 355 | "autobracketing": { 356 | "message": "Автоматический брекетинг" 357 | }, 358 | 359 | "35mmequiv": { 360 | "message": "мм = эквивалент 35мм" 361 | }, 362 | 363 | "program": { 364 | "message": "Программный режим" 365 | }, 366 | 367 | "apriority": { 368 | "message": "Приоритет диафрагмы" 369 | }, 370 | 371 | "spriority": { 372 | "message": "Приоритет выдержки" 373 | }, 374 | 375 | "creative": { 376 | "message": "Творческий режим (приоритет глубины резкости)" 377 | }, 378 | 379 | "action": { 380 | "message": "Спортивный режим (приоритет короткой выдержки)" 381 | }, 382 | 383 | "portrait": { 384 | "message": "Портретный режим" 385 | }, 386 | 387 | "landscape": { 388 | "message": "Пейзажный режим" 389 | }, 390 | 391 | "latlondms": { 392 | "message": "(град., мин., сек.)" 393 | }, 394 | 395 | "latlondm": { 396 | "message": "(град., мин.)" 397 | }, 398 | 399 | "latlondd": { 400 | "message": "(град.)" 401 | }, 402 | 403 | "dirT": { 404 | "message": "° (истинная полночь)" 405 | }, 406 | 407 | "dirM": { 408 | "message": "° (магнитный север)" 409 | }, 410 | 411 | "noTZ": { 412 | "message": "(Часовой пояс не установлен)" 413 | }, 414 | 415 | "unknown": { 416 | "message": "Неизвестно" 417 | }, 418 | 419 | "generalError": { 420 | "message": "Ошибка считывания мета-данных. Показанные мета-данные могут быть не полными. Пожалуйста, сообщите об этой проблеме, по возможности - добавьте проблемное изображение." 421 | }, 422 | 423 | "specialError": { 424 | "message": "Ошибка считывания %S мета-данных. %S Показанные мета-данные могут быть не полными. Пожалуйста, сообщите об этой проблеме, по возможности - добавьте проблемное изображение." 425 | }, 426 | 427 | "UserComment": { 428 | "message": "Комментарий пользователя" 429 | }, 430 | 431 | "ColorSpace": { 432 | "message": "Цветовое пространство" 433 | }, 434 | 435 | "FocalLengthText": { 436 | "message": "Фокусное расстояние" 437 | }, 438 | 439 | "ApertureFNumber": { 440 | "message": "Диафрагма" 441 | }, 442 | 443 | "FocalLength35mmEquiv": { 444 | "message": "Фокусное расстояние (35 мм)" 445 | }, 446 | 447 | "GPSLat": { 448 | "message": "GPS-широта" 449 | }, 450 | 451 | "GPSLon": { 452 | "message": "GPS-долгота" 453 | }, 454 | 455 | "noEXIFdata": { 456 | "message": "Метаданные изображения не найдены" 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /_locales/uk/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionDescription": { 3 | "message": "Шукає та показує EXIF/IPTC/XMP-дані фотографій.", 4 | "description": "Description of the extension." 5 | }, 6 | 7 | "contextMenuText": { 8 | "message": "Перегляд EXIF-даних" 9 | }, 10 | 11 | "Copy": { 12 | "message": "Копіювати" 13 | }, 14 | 15 | "CameraMake": { 16 | "message": "Виробник фотоапарата" 17 | }, 18 | 19 | "CameraModel": { 20 | "message": "Модель фотоапарата" 21 | }, 22 | 23 | "CameraLens": { 24 | "message": "Об'єктив" 25 | }, 26 | 27 | "Software": { 28 | "message": "Програмне Забезпечення" 29 | }, 30 | 31 | "ImageDate": { 32 | "message": "Дата зйомки" 33 | }, 34 | 35 | "ImageOrientation": { 36 | "message": "Орієнтація" 37 | }, 38 | 39 | "ImageBW": { 40 | "message": "Чорно-Біле" 41 | }, 42 | 43 | "FlashUsed": { 44 | "message": "Спалах спрацював" 45 | }, 46 | 47 | "FocalLength": { 48 | "message": "Фокусна (фокальна) відстань" 49 | }, 50 | 51 | "DigitalZoom": { 52 | "message": "Цифровий зум" 53 | }, 54 | 55 | "CCDWidth": { 56 | "message": "Ширина матриці (CCD/ПЗЗ/ПЗС)" 57 | }, 58 | 59 | "ExposureTime": { 60 | "message": "Тривалість експозиції (витримка)" 61 | }, 62 | 63 | "Aperture": { 64 | "message": "Діафрагма" 65 | }, 66 | 67 | "FocusDist": { 68 | "message": "Фокусна відстань" 69 | }, 70 | 71 | "ISOequivalent": { 72 | "message": "ISO-еквівалент" 73 | }, 74 | 75 | "ExposureBias": { 76 | "message": "Експокорекція" 77 | }, 78 | 79 | "WhiteBalance": { 80 | "message": "Баланс білого" 81 | }, 82 | 83 | "LightSource": { 84 | "message": "Джерело світла" 85 | }, 86 | 87 | "MeteringMode": { 88 | "message": "Режим Експозаміру" 89 | }, 90 | 91 | "ExposureProgram": { 92 | "message": "Експозиція" 93 | }, 94 | 95 | "ExposureMode": { 96 | "message": "Режим експозиції" 97 | }, 98 | 99 | "ImageColorSpace": { 100 | "message": "Колірний простір" 101 | }, 102 | 103 | "GPSCoord": { 104 | "message": "GPS-координати" 105 | }, 106 | 107 | "MapLink": { 108 | "message": "Посилання на карту" 109 | }, 110 | 111 | "GPSAlt": { 112 | "message": "GPS-Висота" 113 | }, 114 | 115 | "GPSImgDir": { 116 | "message": "Напрямок погляду" 117 | }, 118 | 119 | "Creator": { 120 | "message": "Автор" 121 | }, 122 | 123 | "City": { 124 | "message": "Місто" 125 | }, 126 | 127 | "Sublocation": { 128 | "message": "Район" 129 | }, 130 | 131 | "ProvinceState": { 132 | "message": "Провінція/Штат" 133 | }, 134 | 135 | "CountryName": { 136 | "message": "Країна" 137 | }, 138 | 139 | "Copyright": { 140 | "message": "Авторське право" 141 | }, 142 | 143 | "Title": { 144 | "message": "Назва" 145 | }, 146 | 147 | "Caption": { 148 | "message": "Заголовок" 149 | }, 150 | 151 | "Comment": { 152 | "message": "Коментар" 153 | }, 154 | 155 | "Instructions": { 156 | "message": "Інструкції" 157 | }, 158 | 159 | "NoData": { 160 | "message": "Не знайдено мета-даних" 161 | }, 162 | 163 | "GPSFormat": { 164 | "message": "Бажаний формат відображення координат" 165 | }, 166 | 167 | "DD.label": { 168 | "message": "Десяткові градуси" 169 | }, 170 | 171 | "DM.label": { 172 | "message": "Градуси, Мінути" 173 | }, 174 | 175 | "DMS.label": { 176 | "message": "Градуси, Мінути, Секунди" 177 | }, 178 | 179 | "windowtitle": { 180 | "message": "FxIF Дані для" 181 | }, 182 | 183 | "orientation0": { 184 | "message": "Невизначено" 185 | }, 186 | 187 | "orientation1": { 188 | "message": "Нормальна" 189 | }, 190 | 191 | "orientation2": { 192 | "message": "Віддзеркалено горизонтально" 193 | }, 194 | 195 | "orientation3": { 196 | "message": "Оберт на 180" 197 | }, 198 | 199 | "orientation4": { 200 | "message": "Віддзеркалено вертикально" 201 | }, 202 | 203 | "orientation5": { 204 | "message": "Transpose - (віддзеркалення + поворот на 90) - віддзеркалено по осі, що проходить через верхній лівий та нижній правий кути квадрата." 205 | }, 206 | 207 | "orientation6": { 208 | "message": "Оберт на 90" 209 | }, 210 | 211 | "orientation7": { 212 | "message": "Transverse - (віддзеркалення + поворот на 90) - віддзеркалено по осі, що проходить через верхній правий і нижній лівий кути квадрата." 213 | }, 214 | 215 | "orientation8": { 216 | "message": "Оберт на 270" 217 | }, 218 | 219 | "infinite": { 220 | "message": "Фокус на нескінченність" 221 | }, 222 | 223 | "meters": { 224 | "message": "м" 225 | }, 226 | 227 | "millimeters": { 228 | "message": "мм" 229 | }, 230 | 231 | "seconds": { 232 | "message": "с" 233 | }, 234 | 235 | "ev": { 236 | "message": "Експозиційне число (EV)" 237 | }, 238 | 239 | "yes": { 240 | "message": "Так" 241 | }, 242 | 243 | "no": { 244 | "message": "Ні" 245 | }, 246 | 247 | "enforced": { 248 | "message": "Примусово" 249 | }, 250 | 251 | "manual": { 252 | "message": "Ручний" 253 | }, 254 | 255 | "auto": { 256 | "message": "Автоматичний" 257 | }, 258 | 259 | "semiauto": { 260 | "message": "Напів-автоматичний" 261 | }, 262 | 263 | "none": { 264 | "message": "Пресети для ББ не використано" 265 | }, 266 | 267 | "daylight": { 268 | "message": "Денне світло" 269 | }, 270 | 271 | "fluorescent": { 272 | "message": "Люмінесцентна лампа" 273 | }, 274 | 275 | "incandescent": { 276 | "message": "Лампа розжарювання" 277 | }, 278 | 279 | "flash": { 280 | "message": "Зі спалахом" 281 | }, 282 | 283 | "fineweather": { 284 | "message": "Ясна погода" 285 | }, 286 | 287 | "cloudy": { 288 | "message": "Хмарно" 289 | }, 290 | 291 | "shade": { 292 | "message": "Тінь" 293 | }, 294 | 295 | "daylightfluorescent": { 296 | "message": "Люмінесцентна лампа денного світла" 297 | }, 298 | 299 | "daywhitefluorescent": { 300 | "message": "Люмінесцентна лампа денного білого світла" 301 | }, 302 | 303 | "coolwhitefluorescent": { 304 | "message": "Люмінесцентна лампа холодно-білого світла" 305 | }, 306 | 307 | "whitefluorescent": { 308 | "message": "Люмінесцентна лампа білого світла" 309 | }, 310 | 311 | "studiotungsten": { 312 | "message": "ISO для студійних ламп розжарювання" 313 | }, 314 | 315 | "noflash": { 316 | "message": "Без спалаху" 317 | }, 318 | 319 | "noreturnlight": { 320 | "message": "Відбите світло спалаху не виявлено датчиком - система не визначила рівень потужності спалаху на основі пробного спалаху, в режимі TTL." 321 | }, 322 | 323 | "returnlight": { 324 | "message": "Відбите світло спалаху виявлено датчиком - система визначила рівень потужності спалаху на основі пробного спалаху, в режимі TTL" 325 | }, 326 | 327 | "redeye": { 328 | "message": "Режим зменшення ефекту червоних очей" 329 | }, 330 | 331 | "average": { 332 | "message": "Узагальнений" 333 | }, 334 | 335 | "centerweight": { 336 | "message": "Центро-Зважений" 337 | }, 338 | 339 | "spot": { 340 | "message": "Точковий" 341 | }, 342 | 343 | "multispot": { 344 | "message": "Багато-Точковий" 345 | }, 346 | 347 | "matrix": { 348 | "message": "Матричний (оцінний, багатозонний)" 349 | }, 350 | 351 | "partial": { 352 | "message": "Частковий" 353 | }, 354 | 355 | "autobracketing": { 356 | "message": "Автоматичний брекетинг" 357 | }, 358 | 359 | "35mmequiv": { 360 | "message": "мм = еквівалент 35мм" 361 | }, 362 | 363 | "program": { 364 | "message": "Програмний режим" 365 | }, 366 | 367 | "apriority": { 368 | "message": "Пріоритет діафрагми" 369 | }, 370 | 371 | "spriority": { 372 | "message": "Пріоритет витримки" 373 | }, 374 | 375 | "creative": { 376 | "message": "Творчий режим (пріоритет глибини різкості)" 377 | }, 378 | 379 | "action": { 380 | "message": "Спортивний режим (пріоритет короткої витримки)" 381 | }, 382 | 383 | "portrait": { 384 | "message": "Портретний режим" 385 | }, 386 | 387 | "landscape": { 388 | "message": "Пейзажний режим" 389 | }, 390 | 391 | "latlondms": { 392 | "message": " (град., мін., сек.)" 393 | }, 394 | 395 | "latlondm": { 396 | "message": " (град., мін.)" 397 | }, 398 | 399 | "latlondd": { 400 | "message": " (град.)" 401 | }, 402 | 403 | "dirT": { 404 | "message": "° (істинна північ)" 405 | }, 406 | 407 | "dirM": { 408 | "message": "° (магнітна північ)" 409 | }, 410 | 411 | "noTZ": { 412 | "message": "(Часовий пояс не встановлено)" 413 | }, 414 | 415 | "unknown": { 416 | "message": "Невідомо" 417 | }, 418 | 419 | "generalError": { 420 | "message": "Помилка зчитування мета-даних. Показані мета-дані можуть бути не повними. Будь ласка, повідомте про цю проблему, за можливості - додайте проблемне зображення." 421 | }, 422 | 423 | "specialError": { 424 | "message": "Помилка зчитування %S мета-даних. %S Показані мета-дані можуть бути не повними. Будь ласка, повідомте про цю проблему, за можливості - додайте проблемне зображення." 425 | }, 426 | 427 | "UserComment": { 428 | "message": "Коментар користувача" 429 | }, 430 | 431 | "ColorSpace": { 432 | "message": "Колірний простір" 433 | }, 434 | 435 | "FocalLengthText": { 436 | "message": "Фокусна відстань" 437 | }, 438 | 439 | "ApertureFNumber": { 440 | "message": "Діафрагма" 441 | }, 442 | 443 | "FocalLength35mmEquiv": { 444 | "message": "Фокусна відстань (35 мм)" 445 | }, 446 | 447 | "GPSLat": { 448 | "message": "GPS-широта" 449 | }, 450 | 451 | "GPSLon": { 452 | "message": "GPS-довгота" 453 | }, 454 | 455 | "noEXIFdata": { 456 | "message": "Метадані зображення не знайдено" 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /backgroundscript.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * 5 | * This Source Code Form is "Incompatible With Secondary Licenses", as 6 | * defined by the Mozilla Public License, v. 2.0. 7 | */ 8 | 9 | browser.contextMenus.create({ 10 | id: "viewexif", 11 | title: browser.i18n.getMessage("contextMenuText"), 12 | contexts: ["image"], 13 | }); 14 | 15 | browser.contextMenus.onClicked.addListener((info, tab) => { 16 | if (info.menuItemId === "viewexif") { 17 | if (info.mediaType !== "image") { 18 | return; 19 | } 20 | 21 | if (info.srcUrl) { 22 | var scripts = [ 23 | "/stringBundle.js", 24 | "/contentscript.js", 25 | "/parseJpeg.js", 26 | "/fxifUtils.js", 27 | "/binExif.js", 28 | "/binIptc.js", 29 | "/xmp.js" 30 | ]; 31 | 32 | var scriptLoadPromises = scripts.map(script => { 33 | return browser.tabs.executeScript(null, { 34 | file: script 35 | }); 36 | }) 37 | 38 | Promise.all(scriptLoadPromises).then(() => { 39 | browser.tabs.sendMessage(tab.id, { 40 | message: "parseImage", 41 | imageURL: info.srcUrl 42 | }); 43 | }); 44 | } 45 | } 46 | }); 47 | 48 | var popupData; 49 | 50 | browser.runtime.onMessage.addListener((request, sender, sendResponse) => { 51 | if (request.message == "EXIFready") { 52 | if (Object.keys(request.data).length == 0) { 53 | popupData = { 54 | "": browser.i18n.getMessage("noEXIFdata") 55 | } 56 | } else { 57 | popupData = request.data; 58 | } 59 | var popupURL = browser.extension.getURL("/popup/popup.html"); 60 | browser.windows.create({ 61 | url: popupURL, 62 | type: "popup", 63 | width: 400, 64 | height: 550 65 | }); 66 | } else if (request.message == "popupReady") { 67 | sendResponse(popupData); 68 | } 69 | }); -------------------------------------------------------------------------------- /binExif.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * 5 | * This Source Code Form is "Incompatible With Secondary Licenses", as 6 | * defined by the Mozilla Public License, v. 2.0. 7 | */ 8 | 9 | /* 10 | * Interpreter for binary EXIF data. 11 | */ 12 | 13 | function exifClass() 14 | { 15 | var fxifUtils = new fxifUtilsClass(); 16 | var loopDetectorArray = new Array(); 17 | 18 | // data formats 19 | const FMT_BYTE = 1; 20 | const FMT_STRING = 2; 21 | const FMT_USHORT = 3; 22 | const FMT_ULONG = 4; 23 | const FMT_URATIONAL = 5; 24 | const FMT_SBYTE = 6; 25 | const FMT_UNDEFINED = 7; 26 | const FMT_SSHORT = 8; 27 | const FMT_SLONG = 9; 28 | const FMT_SRATIONAL = 10; 29 | const FMT_SINGLE = 11; 30 | const FMT_DOUBLE = 12; 31 | 32 | // EXIF tags 33 | const TAG_DESCRIPTION = 0x010E; 34 | const TAG_MAKE = 0x010F; 35 | const TAG_MODEL = 0x0110; 36 | const TAG_ORIENTATION = 0x0112; 37 | const TAG_SOFTWARE = 0x0131; 38 | const TAG_DATETIME = 0x0132; 39 | const TAG_ARTIST = 0x013B; 40 | const TAG_THUMBNAIL_OFFSET = 0x0201; 41 | const TAG_THUMBNAIL_LENGTH = 0x0202; 42 | const TAG_COPYRIGHT = 0x8298; 43 | const TAG_EXPOSURETIME = 0x829A; 44 | const TAG_FNUMBER = 0x829D; 45 | const TAG_EXIF_OFFSET = 0x8769; 46 | const TAG_EXPOSURE_PROGRAM = 0x8822; 47 | const TAG_GPSINFO = 0x8825; 48 | const TAG_ISO_EQUIVALENT = 0x8827; 49 | const TAG_DATETIME_ORIGINAL = 0x9003; 50 | const TAG_DATETIME_DIGITIZED = 0x9004; 51 | const TAG_SHUTTERSPEED = 0x9201; 52 | const TAG_APERTURE = 0x9202; 53 | const TAG_EXPOSURE_BIAS = 0x9204; 54 | const TAG_MAXAPERTURE = 0x9205; 55 | const TAG_SUBJECT_DISTANCE = 0x9206; 56 | const TAG_METERING_MODE = 0x9207; 57 | const TAG_LIGHT_SOURCE = 0x9208; 58 | const TAG_FLASH = 0x9209; 59 | const TAG_FOCALLENGTH = 0x920A; 60 | const TAG_MAKER_NOTE = 0x927C; 61 | const TAG_USERCOMMENT = 0x9286; 62 | const TAG_EXIF_IMAGEWIDTH = 0xa002; 63 | const TAG_EXIF_IMAGELENGTH = 0xa003; 64 | const TAG_INTEROP_OFFSET = 0xa005; 65 | const TAG_FOCALPLANEXRES = 0xa20E; 66 | const TAG_FOCALPLANEUNITS = 0xa210; 67 | const TAG_EXPOSURE_INDEX = 0xa215; 68 | const TAG_EXPOSURE_MODE = 0xa402; 69 | const TAG_WHITEBALANCE = 0xa403; 70 | const TAG_DIGITALZOOMRATIO = 0xa404; 71 | const TAG_FOCALLENGTH_35MM = 0xa405; 72 | const TAG_LENS = 0xfdea; 73 | const TAG_LENSINFO = 0xa432; 74 | const TAG_LENSMAKE = 0xa433; 75 | const TAG_LENSMODEL = 0xa434; 76 | const TAG_COLORSPACE = 0xa001; 77 | const TAG_INTEROPINDEX = 0x0001; 78 | 79 | const TAG_GPS_LAT_REF = 0x0001; 80 | const TAG_GPS_LAT = 0x0002; 81 | const TAG_GPS_LON_REF = 0x0003; 82 | const TAG_GPS_LON = 0x0004; 83 | const TAG_GPS_ALT_REF = 0x0005; 84 | const TAG_GPS_ALT = 0x0006; 85 | const TAG_GPS_IMG_DIR_REF = 0x0010; 86 | const TAG_GPS_IMG_DIR = 0x0011; 87 | 88 | var BytesPerFormat = [0,1,1,2,4,8,1,1,2,4,8,4,8]; 89 | 90 | // Decodes arrays carrying UTF-8 sequences into Unicode strings. 91 | // It also validates sequences and throws an error if it encounters 92 | // invalid encodings. 93 | function utf8BytesToString(utf8data, offset, num) 94 | { 95 | var s = ""; 96 | var c = c1 = c2 = 0; 97 | 98 | for (var i = offset; i < offset + num;) { 99 | c = utf8data[i]; 100 | if (c <= 127) { 101 | if (c != 0) 102 | s += String.fromCharCode(c); 103 | i++; 104 | } 105 | else if ((c >= 194) && (c <= 223)) { 106 | c2 = utf8data[i+1]; 107 | if (c2>>6 != 2) 108 | throw "No valid UTF8-Sequence"; 109 | s += String.fromCharCode(((c&31) << 6) | (c2&63)); 110 | i += 2; 111 | } 112 | else if ((c >= 224) && (c <= 239)) { 113 | c2 = utf8data[i+1]; 114 | if (c2>>6 != 2) 115 | throw "No valid UTF8-Sequence"; 116 | c3 = utf8data[i+2]; 117 | if (c3>>6 != 2) 118 | throw "No valid UTF8-Sequence"; 119 | s += String.fromCharCode(((c&15) << 12) | ((c2&63) << 6) | (c3&63)); 120 | i += 3; 121 | } 122 | else if ((c >= 240) && (c <= 244)) { 123 | c2 = utf8data[i+1]; 124 | if (c2>>6 != 2) 125 | throw "No valid UTF8-Sequence"; 126 | c3 = utf8data[i+2]; 127 | if (c3>>6 != 2) 128 | throw "No valid UTF8-Sequence"; 129 | c4 = utf8data[i+3]; 130 | if (c4>>6 != 2) 131 | throw "No valid UTF8-Sequence"; 132 | s += String.fromCharCode(((c & 7) << 18) | ((c2&63) << 12) | ((c3&63) << 6) | (c4&63)); 133 | i += 4; 134 | } 135 | else { 136 | throw "No valid UTF8-Sequence"; 137 | } 138 | } 139 | 140 | return s; 141 | } 142 | 143 | // Checks if the loopDetectorArray already contains an entry 144 | // which would mean we did already jump there once. 145 | function checkForLoop(val) 146 | { 147 | for (var i = 0; i < loopDetectorArray.length; i++) 148 | { 149 | if (loopDetectorArray[i] == val) 150 | return true; 151 | } 152 | 153 | return false; 154 | } 155 | 156 | function dir_entry_addr(start, entry) 157 | { 158 | return start + 2 + entry * 12; 159 | } 160 | 161 | function ConvertAnyFormat(data, format, offset, components, numbytes, swapbytes, charWidth) 162 | { 163 | // centralised check if the data lays within the data array 164 | if (offset + numbytes > data.length) 165 | { 166 | console.error("Data outside array."); 167 | // throw "Data outside array."; 168 | return; 169 | } 170 | 171 | var value = 0; 172 | 173 | switch (format) { 174 | case FMT_STRING: 175 | // try decoding strings as UTF-8, if it fails, handle them 1:1. 176 | try { 177 | if (charWidth == 1) // don’t try handling Unicode strings as UTF-8 178 | value = utf8BytesToString(data, offset, numbytes); 179 | else 180 | value = fxifUtils.bytesToString(data, offset, numbytes, swapbytes, charWidth); 181 | } 182 | catch(e) 183 | { 184 | console.error("catch"); 185 | value = fxifUtils.bytesToString(data, offset, numbytes, swapbytes, charWidth); 186 | } 187 | // strip trailing whitespace 188 | value = value.replace(/\s+$/, ''); 189 | break; 190 | 191 | case FMT_UNDEFINED: // treat as string 192 | value = fxifUtils.bytesToString(data, offset, numbytes, swapbytes, charWidth); 193 | // strip trailing whitespace 194 | value = value.replace(/\s+$/, ''); 195 | break; 196 | 197 | case FMT_SBYTE: value = data[offset]; break; 198 | case FMT_BYTE: value = data[offset]; break; 199 | 200 | case FMT_USHORT: value = fxifUtils.read16(data, offset, swapbytes); break; 201 | case FMT_ULONG: value = fxifUtils.read32(data, offset, swapbytes); break; 202 | 203 | case FMT_URATIONAL: 204 | case FMT_SRATIONAL: 205 | { 206 | // It sometimes happens that there are multiple rationals contained. 207 | // So go for multiple here and convert back later. 208 | var values = new Array(); 209 | 210 | for (var i = 0; i < components; i++) { 211 | var Num, Den; 212 | Num = fxifUtils.read32(data, offset+i*8, swapbytes); 213 | Den = fxifUtils.read32(data, offset+i*8+4, swapbytes); 214 | if (Den == 0) { 215 | values[i] = 0; 216 | } else { 217 | values[i] = Num / Den; 218 | } 219 | } 220 | 221 | if (components == 1) 222 | value = values[0]; 223 | else 224 | value = values; 225 | break; 226 | } 227 | 228 | case FMT_SSHORT: value = fxifUtils.read16(data, offset, swapbytes); break; 229 | case FMT_SLONG: value = fxifUtils.read32(data, offset, swapbytes); break; 230 | 231 | // ignore, probably never used 232 | case FMT_SINGLE: value = 0; break; 233 | case FMT_DOUBLE: value = 0; break; 234 | } 235 | 236 | return value; 237 | } 238 | 239 | function readGPSDir(dataObj, data, dirStart, swapbytes) 240 | { 241 | var numEntries = fxifUtils.read16(data, dirStart, swapbytes); 242 | // check if all entries lay within the data array 243 | if (dirStart + 2 + numEntries * 12 > data.length) 244 | // so something is wrong – limit numEntries or bail out? 245 | // let’s try with limiting it 246 | numEntries = Math.floor((data.length - (dirStart + 2)) / (numEntries * 12)); 247 | var gpsLatHemisphere = 'N', gpsLonHemisphere = 'E', gpsAltReference = 0, gpsImgDirReference = 'M'; 248 | var gpsLat, gpsLon, gpsAlt; 249 | var vals = new Array(); 250 | 251 | for (var i = 0; i < numEntries; i++) { 252 | var entry = dir_entry_addr(dirStart, i); 253 | var tag = fxifUtils.read16(data, entry, swapbytes); 254 | var format = fxifUtils.read16(data, entry+2, swapbytes); 255 | var components = fxifUtils.read32(data, entry+4, swapbytes); 256 | 257 | if (format >= BytesPerFormat.length) 258 | continue; 259 | 260 | var nbytes = components * BytesPerFormat[format]; 261 | var valueoffset; 262 | 263 | if (nbytes <= 4) // stored in the entry 264 | valueoffset = entry + 8; 265 | else 266 | valueoffset = fxifUtils.read32(data, entry + 8, swapbytes); 267 | 268 | // try 269 | // { 270 | var val = ConvertAnyFormat(data, format, valueoffset, components, nbytes, swapbytes, 1); 271 | // If ConvertAnyFormat() encounters that data lies 272 | // outside of the data array, it returns undefined. 273 | // We don’t want to assign this but to try again with 274 | // the next tag. 275 | if (val === undefined) 276 | continue; 277 | 278 | switch(tag) { 279 | case TAG_GPS_LAT_REF: 280 | gpsLatHemisphere = val; 281 | break; 282 | 283 | case TAG_GPS_LON_REF: 284 | gpsLonHemisphere = val; 285 | break; 286 | 287 | case TAG_GPS_ALT_REF: 288 | gpsAltReference = val; 289 | break; 290 | 291 | case TAG_GPS_IMG_DIR_REF: 292 | gpsImgDirReference = val; 293 | break; 294 | 295 | case TAG_GPS_LAT: 296 | case TAG_GPS_LON: 297 | // data is saved as three 64bit rationals -> 24 bytes 298 | // so we've to do another two ConvertAnyFormat() ourself 299 | // e.g. 0x0b / 0x01, 0x07 / 0x01, 0x011c4d / 0x0c92 300 | // but can also be only 0x31 / 0x01, 0x3d8ba / 0x2710, 0x0 / 0x01 301 | var gpsval = val[0] * 3600 + val[1] * 60 + val[2]; 302 | // var gpsval = val * 3600; 303 | // gpsval += ConvertAnyFormat(data, format, valueoffset+8, nbytes, swapbytes) * 60; 304 | // gpsval += ConvertAnyFormat(data, format, valueoffset+16, nbytes, swapbytes); 305 | vals[tag] = gpsval; 306 | break; 307 | 308 | case TAG_GPS_ALT: 309 | vals[tag] = val; 310 | break; 311 | 312 | case TAG_GPS_IMG_DIR: 313 | vals[tag] = val; 314 | break; 315 | 316 | default: 317 | break; 318 | } 319 | // } catch(e){} 320 | // so something is wrong – bail out or step over this value? 321 | // let’s try with just stepping over this value 322 | } 323 | 324 | // use dms format by default 325 | var degFormat = "dms"; 326 | var degFormatter = fxifUtils.dd2dms; 327 | try { 328 | // 0 = DMS, 1 = DD, 2 = DM 329 | if (fxifUtils.getPreferences().getIntPref("gpsFormat") == 1) 330 | { 331 | // but dd if the user wants that 332 | degFormat = "dd"; 333 | degFormatter = fxifUtils.dd2dd; 334 | } 335 | else if (fxifUtils.getPreferences().getIntPref("gpsFormat") == 2) 336 | { 337 | // but dd if the user wants that 338 | degFormat = "dm"; 339 | degFormatter = fxifUtils.dd2dm; 340 | } 341 | } catch(e){} 342 | // now output all existing values 343 | if (vals[TAG_GPS_LAT] != undefined) { 344 | var gpsArr = degFormatter(vals[TAG_GPS_LAT]); 345 | gpsArr.push(gpsLatHemisphere); 346 | dataObj.GPSLat = stringBundle.getFormattedString("latlon"+degFormat, gpsArr); 347 | } 348 | if (vals[TAG_GPS_LON] != undefined) { 349 | var gpsArr = degFormatter(vals[TAG_GPS_LON]); 350 | gpsArr.push(gpsLonHemisphere); 351 | dataObj.GPSLon = stringBundle.getFormattedString("latlon"+degFormat, gpsArr); 352 | } 353 | if (vals[TAG_GPS_ALT] != undefined) { 354 | dataObj.GPSAlt = stringBundle.getFormattedString("meters", [vals[TAG_GPS_ALT] * (gpsAltReference ? -1.0 : 1.0)]); 355 | } 356 | if (vals[TAG_GPS_IMG_DIR] != undefined && (gpsImgDirReference == 'M' || gpsImgDirReference == 'T')) { 357 | dataObj.GPSImgDir = stringBundle.getFormattedString("dir"+gpsImgDirReference, [vals[TAG_GPS_IMG_DIR]]); 358 | } 359 | // Get the straight decimal values without rounding. 360 | // For creating links to map services. 361 | if (vals[TAG_GPS_LAT] != undefined && 362 | vals[TAG_GPS_LON] != undefined) { 363 | dataObj.GPSPureDdLat = vals[TAG_GPS_LAT] / 3600 * (gpsLatHemisphere == 'N' ? 1.0 : -1.0); 364 | dataObj.GPSPureDdLon = vals[TAG_GPS_LON] / 3600 * (gpsLonHemisphere == 'E' ? 1.0 : -1.0); 365 | } 366 | } 367 | 368 | /* Reads the Canon Tags IFD. 369 | EOS60D_YIMG_0007.JPG 370 | 0x927c at pos 0x22a 371 | Type undef (7) 372 | Components 0x1e2c 373 | Offset 0x0382 374 | Real MN-Offset: 0x038e 375 | 376 | */ 377 | /* CanonExifDir (MakerNotes) are always in Intel (little endian) byte order, 378 | * so always do swapbytes. 379 | */ 380 | function readCanonExifDir (dataObj, data, dirStart, makerNotesLen) 381 | { 382 | // Canon EXIF tags 383 | const TAG_CAMERA_INFO = 0x000d; 384 | const TAG_LENS_MODEL = 0x0095; 385 | const TAG_CANON_MODEL_ID = 0x0010; 386 | 387 | var ntags = 0; 388 | // Canon MakerNotes are always in Intel (little endian) byte order 389 | var swapbytes = true; 390 | var numEntries = fxifUtils.read16(data, dirStart, swapbytes); 391 | // check if all entries lay within the data array 392 | if (dirStart + 2 + numEntries * 12 > data.length) 393 | // so something is wrong – limit numEntries or bail out? 394 | // let’s try with limiting it 395 | numEntries = Math.floor((data.length - (dirStart + 2)) / (numEntries * 12)); 396 | 397 | var entryOffset = FixCanonMakerNotesBase(data, dirStart, makerNotesLen); 398 | 399 | for (var i = 0; i < numEntries; i++) { 400 | var entry = dir_entry_addr(dirStart, i); 401 | var tag = fxifUtils.read16(data, entry, swapbytes); 402 | var format = fxifUtils.read16(data, entry+2, swapbytes); 403 | var components = fxifUtils.read32(data, entry+4, swapbytes); 404 | 405 | if (format >= BytesPerFormat.length) 406 | continue; 407 | 408 | var nbytes = components * BytesPerFormat[format]; 409 | var valueoffset; 410 | if (nbytes <= 4) { 411 | // stored in the entry 412 | valueoffset = entry + 8; 413 | } 414 | else { 415 | // stored in data area 416 | valueoffset = fxifUtils.read32(data, entry + 8, swapbytes) + entryOffset; 417 | } 418 | 419 | var val = ConvertAnyFormat(data, format, valueoffset, components, nbytes, swapbytes, 1); 420 | // If ConvertAnyFormat() encounters that data lies 421 | // outside of the data array, it returns undefined. 422 | // We don’t want to assign this but to try again with 423 | // the next tag. 424 | if (val === undefined) 425 | continue; 426 | 427 | ntags++; 428 | switch(tag) { 429 | case TAG_CAMERA_INFO: 430 | dataObj.CameraInfo = val; 431 | break; 432 | case TAG_CANON_MODEL_ID: 433 | dataObj.ModelID = val; 434 | break; 435 | case TAG_LENS_MODEL: 436 | dataObj.Lens = val; 437 | break; 438 | default: 439 | ntags--; 440 | } 441 | } 442 | 443 | /* List of Canon Model IDs 444 | 0x80000001 = EOS-1D 445 | 0x80000167 = EOS-1DS 446 | 0x80000168 = EOS 10D 447 | 0x80000169 = EOS-1D Mark III 448 | 0x80000170 = EOS Digital Rebel / 300D / Kiss Digital 449 | 0x80000174 = EOS-1D Mark II 450 | 0x80000175 = EOS 20D 451 | 0x80000176 = EOS Digital Rebel XSi / 450D / Kiss X2 452 | 0x80000188 = EOS-1Ds Mark II 453 | 0x80000189 = EOS Digital Rebel XT / 350D / Kiss Digital N 454 | 0x80000190 = EOS 40D 455 | 0x80000213 = EOS 5D 456 | 0x80000215 = EOS-1Ds Mark III 457 | 0x80000218 = EOS 5D Mark II 458 | 0x80000232 = EOS-1D Mark II N 459 | 0x80000234 = EOS 30D 460 | 0x80000236 = EOS Digital Rebel XTi / 400D / Kiss Digital X 461 | 0x80000250 = EOS 7D 462 | 0x80000252 = EOS Rebel T1i / 500D / Kiss X3 463 | 0x80000254 = EOS Rebel XS / 1000D / Kiss F 464 | 0x80000261 = EOS 50D 465 | 0x80000270 = EOS Rebel T2i / 550D / Kiss X4 466 | 0x80000281 = EOS-1D Mark IV 467 | 0x80000287 = EOS 60D 468 | */ 469 | 470 | return ntags; 471 | } 472 | 473 | // This functions code is derived from Phil Harveys Image::ExifTool::MakerNotes FixBase() 474 | function FixCanonMakerNotesBase (data, dirStart, makerNotesLen) 475 | { 476 | var swapbytes = true; 477 | var fix = 0; 478 | 479 | if (makerNotesLen > 8) 480 | { 481 | var footerPtr = dirStart + makerNotesLen - 8; 482 | var footer = fxifUtils.bytesToStringWithNull(data, footerPtr, 8); 483 | if (footer.search(/^(II\x2a\0|MM\0\x2a)/) != -1) // check for TIFF footer 484 | // footer.substr(0, 2) == GetByteOrder()) # validate byte ordering 485 | { 486 | var offsetFromFooter = ConvertAnyFormat(data, FMT_ULONG, footerPtr + 4, 0, 0, swapbytes, 1); 487 | fix = dirStart - offsetFromFooter; 488 | if (fix == 0) 489 | return 0; 490 | // Picasa and ACDSee have a bug where they update other offsets without 491 | // updating the TIFF footer (PH - 2009/02/25), so test for this case: 492 | // validate Canon maker note footer fix by checking offset of last value 493 | // var maxPt = $valPtrs[-1] + $$valBlock{$valPtrs[-1]}; 494 | // compare to end of maker notes, taking 8-byte footer into account 495 | //Autumn1.jpg 496 | //dirStart: 4814, dirLen: 4876, maxPt: 5526, dataPos: 0 497 | // var endDiff = footerPtr - (maxPt - dataPos); 498 | // ignore footer offset only if end difference is exactly correct 499 | // (allow for possible padding byte, although I have never seen this) 500 | /* if (endDiff == 0 || endDiff == 1) { 501 | alert('Canon maker note footer may be invalid (ignored)'); 502 | // $exifTool->Warn('Canon maker note footer may be invalid (ignored)', 1); 503 | return 0; 504 | } 505 | */ 506 | } 507 | } 508 | 509 | return fix; 510 | } 511 | 512 | /* 513 | D3S_141805.jpg 514 | 0x927c at pos 0x24b 515 | Type undef (7) 516 | Components 0x8612 517 | Offset 0x0337 518 | Real MN-Offset: 0x0356 519 | 520 | */ 521 | 522 | /* Offsets to Nikon Maker Notes point to the five bytes "Nikon" followed 523 | by a null. This is followed by two bytes denoting the Exif Version as 524 | text, e.g. 0x0210, followed by two null. 525 | Then (after the above 0x0a bytes) a normal TIFF header follows with an IFD 526 | and all the data. The data in this block is has its own byte order which 527 | might be different from the one in the rest of the Exif header as denoted 528 | in this TIFF header (which typically is Motorola byte order for Nikon). 529 | */ 530 | function readNikonExifDir (dataObj, data, dirStart, swapbytes) 531 | { 532 | // Is it really Nikon? 533 | if (header == 'Nikon\0') { 534 | // step over next four bytes denoting the version (e.g. 0x02100000) 535 | // 8 byte TIFF header 536 | var exifData = bis.readByteArray(len - 6); 537 | 538 | // first two determine byte order 539 | swapbytes = fxifUtils.read16(exifData, 0, false) == INTEL_BYTE_ORDER; 540 | 541 | // next two bytes are always 0x002A 542 | // offset to Image File Directory (includes the previous 8 bytes) 543 | var ifd_ofs = fxifUtils.read32(exifData, 4, swapbytes); 544 | var exifReader = new exifClass(stringBundle); 545 | try { 546 | exifReader.readExifDir(dataObj, exifData, ifd_ofs, swapbytes); 547 | } 548 | catch(ex) { 549 | pushError(dataObj, "EXIF", ex); 550 | } 551 | fxifUtils.exifDone = true; 552 | } 553 | } 554 | 555 | 556 | /* Reads the actual EXIF tags. 557 | Also extracts tags for textual informations like 558 | By, Caption, Headline, Copyright. 559 | But doesn't overwrite those fields when already populated 560 | by IPTC-NAA or IPTC4XMP. 561 | */ 562 | this.readExifDir = function (dataObj, data, dirStart, swapbytes) 563 | { 564 | loopDetectorArray.push(dirStart); 565 | 566 | var ntags = 0; 567 | var numEntries = fxifUtils.read16(data, dirStart, swapbytes); 568 | // check if all entries lay within the data array 569 | var tst = dirStart + 2 + numEntries * 12; 570 | if (dirStart + 2 + numEntries * 12 > data.length) 571 | { 572 | // so something is wrong – limit numEntries or bail out? 573 | // let’s try with limiting it 574 | numEntries = Math.floor((data.length - (dirStart + 2)) / (numEntries * 12)); 575 | } 576 | 577 | var interopIndex = ""; 578 | var colorSpace = 0; 579 | var exifDateTime = 0; 580 | var exifDateTimeOrig = 0; 581 | var lensInfo; 582 | for (var i = 0; i < numEntries; i++) { 583 | var entry = dir_entry_addr(dirStart, i); 584 | var tag = fxifUtils.read16(data, entry, swapbytes); 585 | 586 | var format = fxifUtils.read16(data, entry+2, swapbytes); 587 | var components = fxifUtils.read32(data, entry+4, swapbytes); 588 | 589 | if (format >= BytesPerFormat.length) 590 | continue; 591 | 592 | var nbytes = components * BytesPerFormat[format]; 593 | var valueoffset; 594 | 595 | if (nbytes <= 4) 596 | { 597 | // stored in the entry 598 | valueoffset = entry + 8; 599 | } 600 | else { 601 | // stored in data area 602 | valueoffset = fxifUtils.read32(data, entry + 8, swapbytes); 603 | } 604 | 605 | var val = ConvertAnyFormat(data, format, valueoffset, components, nbytes, swapbytes, 1); 606 | // If ConvertAnyFormat() encounters that data lies 607 | // outside of the data array, it returns undefined. 608 | // We don’t want to assign this but to try again with 609 | // the next tag. 610 | if (val === undefined) 611 | continue; 612 | 613 | ntags++; 614 | switch(tag) 615 | { 616 | case TAG_MAKE: 617 | dataObj.Make = val; 618 | break; 619 | 620 | case TAG_MODEL: 621 | dataObj.Model = val; 622 | break; 623 | 624 | case TAG_SOFTWARE: 625 | dataObj.Software = val; 626 | break; 627 | 628 | case TAG_DATETIME_ORIGINAL: 629 | exifDateTimeOrig = val; 630 | break; 631 | 632 | case TAG_DATETIME_DIGITIZED: 633 | case TAG_DATETIME: 634 | exifDateTime = val; 635 | break; 636 | 637 | case TAG_USERCOMMENT: 638 | var charWidth = 1; 639 | if (val.search(/^UNICODE\s*/) >= 0) { 640 | charWidth = 2; 641 | } 642 | // strip leading character code string 643 | var ccStringLen = 0; 644 | if (nbytes >= 8) { 645 | ccStringLen = 8; 646 | } 647 | dataObj.UserComment = ConvertAnyFormat(data, format, valueoffset + ccStringLen, components, nbytes - ccStringLen, swapbytes, charWidth); 648 | break; 649 | 650 | case TAG_FNUMBER: 651 | dataObj.ApertureFNumber = "ƒ/" + parseFloat(val).toFixed(1); 652 | break; 653 | 654 | // only use these if we don't have the previous 655 | case TAG_APERTURE: 656 | if (!dataObj.ApertureFNumber) { 657 | dataObj.ApertureFNumber = "ƒ/" + Math.exp((parseFloat(val) * Math.LN2 * 0.5)).toFixed(1); 658 | } 659 | break; 660 | 661 | // only use these if we don't have the previous 662 | case TAG_MAXAPERTURE: 663 | if (!dataObj.ApertureFNumber) { 664 | dataObj.ApertureFNumber = "ƒ/" + Math.exp((parseFloat(val) * Math.LN2 * 0.5)).toFixed(1); 665 | } 666 | break; 667 | 668 | case TAG_FOCALLENGTH: 669 | dataObj.FocalLength = parseFloat(val); 670 | break; 671 | 672 | case TAG_SUBJECT_DISTANCE: 673 | if (val < 0) { 674 | dataObj.Distance = stringBundle.getString("infinite"); 675 | } 676 | else { 677 | dataObj.Distance = stringBundle.getFormattedString("meters", [val]); 678 | } 679 | break; 680 | 681 | case TAG_EXPOSURETIME: 682 | var et = ""; 683 | val = parseFloat(val); 684 | if (val < 0.010) { 685 | et = stringBundle.getFormattedString("seconds", [val.toFixed(4)]); 686 | }else { 687 | et = stringBundle.getFormattedString("seconds", [val.toFixed(3)]); 688 | } 689 | if (val <= 0.5){ 690 | et += " (1/" + Math.floor(0.5 + 1/val).toFixed(0) + ")"; 691 | } 692 | dataObj.ExposureTime = et; 693 | break; 694 | 695 | case TAG_SHUTTERSPEED: 696 | if (!dataObj.ExposureTime) { 697 | dataObj.ExposureTime = stringBundle.getFormattedString("seconds", [(1.0 / Math.exp(parseFloat(val) * Math.log(2))).toFixed(4)]); 698 | } 699 | break; 700 | 701 | case TAG_FLASH: 702 | // Bit 0 indicates the flash firing status, 703 | // bits 1 and 2 indicate the flash return status, 704 | // bits 3 and 4 indicate the flash mode, 705 | // bit 5 indicates whether the flash function is present, 706 | // bit 6 indicates "red eye" mode. 707 | if (val >= 0) { 708 | var fu; 709 | var addfunc = new Array(); 710 | if (val & 0x01) { 711 | fu = stringBundle.getString("yes"); 712 | 713 | if (val & 0x18 == 0x18) 714 | addfunc.push(stringBundle.getString("auto")); 715 | else if (val & 0x8) 716 | addfunc.push(stringBundle.getString("enforced")); 717 | 718 | if (val & 0x40) 719 | addfunc.push(stringBundle.getString("redeye")); 720 | 721 | if (val & 0x06 == 0x06) 722 | addfunc.push(stringBundle.getString("returnlight")); 723 | else if (val & 0x04) 724 | addfunc.push(stringBundle.getString("noreturnlight")); 725 | } 726 | else { 727 | fu = stringBundle.getString("no"); 728 | 729 | if (val & 0x20) 730 | addfunc.push(stringBundle.getString("noflash")); 731 | else if (val & 0x18 == 0x18) 732 | addfunc.push(stringBundle.getString("auto")); 733 | else if (val & 0x10) 734 | addfunc.push(stringBundle.getString("enforced")); 735 | } 736 | 737 | if (addfunc.length) 738 | fu += " (" + addfunc.join(", ") + ")"; 739 | 740 | dataObj.FlashUsed = fu; 741 | } 742 | break; 743 | 744 | case TAG_ORIENTATION: 745 | if (!dataObj.Orientation && val > 0) { 746 | if (val <= 8) 747 | dataObj.Orientation = stringBundle.getString("orientation" + val); 748 | else 749 | dataObj.Orientation = stringBundle.getString("unknown") + " (" + val + ")"; 750 | } 751 | break; 752 | /* 753 | case TAG_EXIF_IMAGELENGTH: 754 | dataObj.Length = val; 755 | break; 756 | 757 | case TAG_EXIF_IMAGEWIDTH: 758 | dataObj.Width = val; 759 | break; 760 | */ 761 | case TAG_FOCALPLANEXRES: 762 | dataObj.FocalPlaneXRes = val; 763 | break; 764 | 765 | case TAG_FOCALPLANEUNITS: 766 | switch(val) { 767 | case 1: dataObj.FocalPlaneUnits = 25.4; break; // inch 768 | case 2: 769 | // According to the information I was using, 2 means meters. 770 | // But looking at the Cannon powershot's files, inches is the only 771 | // sensible value. 772 | dataObj.FocalPlaneUnits = 25.4; 773 | break; 774 | 775 | case 3: dataObj.FocalPlaneUnits = 10; break; // centimeter 776 | case 4: dataObj.FocalPlaneUnits = 1; break; // millimeter 777 | case 5: dataObj.FocalPlaneUnits = .001; break; // micrometer 778 | } 779 | break; 780 | 781 | case TAG_EXPOSURE_BIAS: 782 | val = parseFloat(val); 783 | if (val == 0) 784 | dataObj.ExposureBias = stringBundle.getString("none"); 785 | else 786 | // add a + sign before positive values 787 | dataObj.ExposureBias = (val > 0 ? '+' : '') + stringBundle.getFormattedString("ev", [val.toFixed(2)]); 788 | break; 789 | 790 | case TAG_WHITEBALANCE: 791 | switch(val) { 792 | case 0: 793 | dataObj.WhiteBalance = stringBundle.getString("auto"); 794 | break; 795 | case 1: 796 | dataObj.WhiteBalance = stringBundle.getString("manual"); 797 | break; 798 | } 799 | break; 800 | 801 | case TAG_LIGHT_SOURCE: 802 | switch(val) { 803 | case 1: 804 | dataObj.LightSource = stringBundle.getString("daylight"); 805 | break; 806 | case 2: 807 | dataObj.LightSource = stringBundle.getString("fluorescent"); 808 | break; 809 | case 3: 810 | dataObj.LightSource = stringBundle.getString("incandescent"); 811 | break; 812 | case 4: 813 | dataObj.LightSource = stringBundle.getString("flash"); 814 | break; 815 | case 9: 816 | dataObj.LightSource = stringBundle.getString("fineweather"); 817 | break; 818 | case 10: 819 | dataObj.LightSource = stringBundle.getString("cloudy"); 820 | break; 821 | case 11: 822 | dataObj.LightSource = stringBundle.getString("shade"); 823 | break; 824 | case 12: 825 | dataObj.LightSource = stringBundle.getString("daylightfluorescent"); 826 | break; 827 | case 13: 828 | dataObj.LightSource = stringBundle.getString("daywhitefluorescent"); 829 | break; 830 | case 14: 831 | dataObj.LightSource = stringBundle.getString("coolwhitefluorescent"); 832 | break; 833 | case 15: 834 | dataObj.LightSource = stringBundle.getString("whitefluorescent"); 835 | break; 836 | case 24: 837 | dataObj.LightSource = stringBundle.getString("studiotungsten"); 838 | break; 839 | default:; //Quercus: 17-1-2004 There are many more modes for this, check Exif2.2 specs 840 | // If it just says 'unknown' or we don't know it, then 841 | // don't bother showing it - it doesn't add any useful information. 842 | } 843 | break; 844 | 845 | case TAG_METERING_MODE: 846 | switch(val) { 847 | case 0: 848 | dataObj.MeteringMode = stringBundle.getString("unknown"); 849 | break; 850 | case 1: 851 | dataObj.MeteringMode = stringBundle.getString("average"); 852 | break; 853 | case 2: 854 | dataObj.MeteringMode = stringBundle.getString("centerweight"); 855 | break; 856 | case 3: 857 | dataObj.MeteringMode = stringBundle.getString("spot"); 858 | break; 859 | case 3: 860 | dataObj.MeteringMode = stringBundle.getString("multispot"); 861 | break; 862 | case 5: 863 | dataObj.MeteringMode = stringBundle.getString("matrix"); 864 | break; 865 | case 6: 866 | dataObj.MeteringMode = stringBundle.getString("partial"); 867 | break; 868 | } 869 | break; 870 | 871 | case TAG_EXPOSURE_PROGRAM: 872 | switch(val) { 873 | case 1: 874 | dataObj.ExposureProgram = stringBundle.getString("manual"); 875 | break; 876 | case 2: 877 | dataObj.ExposureProgram = stringBundle.getString("program") + " (" 878 | + stringBundle.getString("auto") + ")"; 879 | break; 880 | case 3: 881 | dataObj.ExposureProgram = stringBundle.getString("apriority") 882 | + " (" + stringBundle.getString("semiauto") + ")"; 883 | break; 884 | case 4: 885 | dataObj.ExposureProgram = stringBundle.getString("spriority") 886 | + " (" + stringBundle.getString("semiauto") +")"; 887 | break; 888 | case 5: 889 | dataObj.ExposureProgram = stringBundle.getString("creative"); 890 | break; 891 | case 6: 892 | dataObj.ExposureProgram = stringBundle.getString("action"); 893 | break; 894 | case 7: 895 | dataObj.ExposureProgram = stringBundle.getString("portrait"); 896 | break; 897 | case 8: 898 | dataObj.ExposureProgram = stringBundle.getString("landscape"); 899 | break; 900 | default: 901 | break; 902 | } 903 | break; 904 | 905 | case TAG_EXPOSURE_INDEX: 906 | if (!dataObj.ExposureIndex) 907 | { 908 | try 909 | { 910 | // I know of at least one image where this information 911 | // is present as string instead of number. 912 | dataObj.ExposureIndex = val.toFixed(0); 913 | } 914 | catch(e) 915 | { 916 | var tmp = parseInt(val); 917 | if (!isNaN(tmp)) 918 | dataObj.ExposureIndex = tmp; 919 | } 920 | } 921 | break; 922 | 923 | case TAG_EXPOSURE_MODE: 924 | switch(val) { 925 | case 0: //Automatic 926 | break; 927 | case 1: 928 | dataObj.ExposureMode = stringBundle.getString("manual"); 929 | break; 930 | case 2: 931 | dataObj.ExposureMode = stringBundle.getString("autobracketing"); 932 | break; 933 | } 934 | break; 935 | 936 | case TAG_ISO_EQUIVALENT: 937 | try 938 | { 939 | // I know of at least one image where this information 940 | // is present as string instead of number. 941 | dataObj.ISOequivalent = val.toFixed(0); 942 | } 943 | catch(e) 944 | { 945 | var tmp = parseInt(val); 946 | if (!isNaN(tmp)) 947 | dataObj.ISOequivalent = tmp; 948 | } 949 | break; 950 | 951 | case TAG_DIGITALZOOMRATIO: 952 | if (val > 1) { 953 | dataObj.DigitalZoomRatio = val.toFixed(3) + "x"; 954 | } 955 | break; 956 | 957 | case TAG_THUMBNAIL_OFFSET: 958 | break; 959 | 960 | case TAG_THUMBNAIL_LENGTH: 961 | break; 962 | 963 | case TAG_FOCALLENGTH_35MM: 964 | dataObj.FocalLength35mmEquiv = val; 965 | break; 966 | 967 | case TAG_LENSINFO: 968 | lensInfo = val; 969 | break; 970 | 971 | case TAG_LENSMAKE: 972 | dataObj.LensMake = val; 973 | break; 974 | 975 | case TAG_LENSMODEL: 976 | dataObj.LensModel = val; 977 | break; 978 | 979 | case TAG_EXIF_OFFSET: 980 | case TAG_INTEROP_OFFSET: 981 | // Prevent loops, where we directly or indirectly point to an EXIF directory where 982 | // we've already been. It has happened that we recursed thousands of times because 983 | // this tag pointed to its own start. 984 | if (!checkForLoop(val)) 985 | { 986 | // check if it jumps at least to the beginning of actual data 987 | // and at most on the last byte of the array 988 | if (val >= 8 && val < data.length) 989 | ntags += this.readExifDir(dataObj, data, val, swapbytes); 990 | } 991 | break; 992 | 993 | case TAG_GPSINFO: 994 | // check if jumps at least at the beginning of actual data 995 | // and at most on the last byte of the array 996 | if (val >= 8 && val < data.length) 997 | readGPSDir(dataObj, data, val, swapbytes); 998 | break; 999 | 1000 | case TAG_ARTIST: 1001 | if (!dataObj.Creator) 1002 | dataObj.Creator = val; 1003 | break; 1004 | 1005 | case TAG_COPYRIGHT: 1006 | if (!dataObj.Copyright) 1007 | dataObj.Copyright = val; 1008 | break; 1009 | 1010 | case TAG_DESCRIPTION: 1011 | if (!dataObj.Caption) 1012 | dataObj.Caption = val; 1013 | break; 1014 | 1015 | case TAG_COLORSPACE: 1016 | if (!dataObj.ColorSpace) 1017 | { 1018 | if (val == 1) 1019 | dataObj.ColorSpace = "sRGB"; 1020 | else 1021 | colorSpace = val; 1022 | } 1023 | break; 1024 | 1025 | case TAG_MAKER_NOTE: 1026 | // Currently only Canon MakerNotes are supported, so filter for this 1027 | // maker. 1028 | if (dataObj.Make == 'Canon') 1029 | { 1030 | // This tags format is often given as undefined or zero with 1031 | // some weird numbers or zeros as components. This makes the 1032 | // code before this switch to generate strange offsets. 1033 | // Therefore use value at entry + 8 directly. 1034 | // This should work in any case and really does for the available test images. 1035 | var val = ConvertAnyFormat(data, FMT_ULONG, entry + 8, 0, 0, swapbytes, 1); 1036 | var dirLen = ConvertAnyFormat(data, FMT_ULONG, entry + 4, 0, 0, swapbytes, 1); 1037 | 1038 | // check if it jumps at least at the beginning of the actual 1039 | // data and at most on the last byte of the array 1040 | if (val >= 8 && val + dirLen < data.length) 1041 | ntags += readCanonExifDir(dataObj, data, val, dirLen); 1042 | } 1043 | break; 1044 | 1045 | case TAG_INTEROPINDEX: 1046 | interopIndex = val; 1047 | break; 1048 | 1049 | default: 1050 | ntags--; 1051 | } 1052 | } 1053 | 1054 | 1055 | // Now we can be sure to have read all data. So fill 1056 | // some properties which depend on more than one field 1057 | // or a field by various fields ordered by priority. 1058 | 1059 | if (!dataObj.Date) 1060 | { 1061 | if (exifDateTimeOrig) 1062 | dataObj.Date = exifDateTimeOrig; 1063 | else if (exifDateTime) 1064 | dataObj.Date = exifDateTime; 1065 | 1066 | if (dataObj.Date) 1067 | dataObj.Date = dataObj.Date.replace(/:(\d{2}):/, "-$1-") + " " + stringBundle.getString("noTZ"); 1068 | } 1069 | 1070 | if (colorSpace != 0) 1071 | { 1072 | if (dataObj.ColorSpace == 2 || 1073 | dataObj.ColorSpace == 65535 && interopIndex.search(/^R03$/)) 1074 | dataObj.ColorSpace = "Adobe RGB"; 1075 | } 1076 | 1077 | if (dataObj.FocalLength) { 1078 | dataObj.FocalLength = parseFloat(dataObj.FocalLength); 1079 | var fl = stringBundle.getFormattedString("millimeters", [dataObj.FocalLength.toFixed(1)]); 1080 | if (dataObj.FocalLength35mmEquiv) { 1081 | dataObj.FocalLength35mmEquiv = parseFloat(dataObj.FocalLength35mmEquiv); 1082 | fl += " " + stringBundle.getFormattedString("35mmequiv", [dataObj.FocalLength35mmEquiv.toFixed(0)]); 1083 | } 1084 | 1085 | dataObj.FocalLengthText = fl; 1086 | } 1087 | 1088 | if (dataObj.LensMake) { 1089 | dataObj.Lens = dataObj.LensMake; 1090 | } 1091 | 1092 | if (!dataObj.Lens) 1093 | { 1094 | if (dataObj.LensModel) { 1095 | if (dataObj.Lens) 1096 | dataObj.Lens += " "; 1097 | else 1098 | dataObj.Lens = ""; 1099 | 1100 | dataObj.Lens += dataObj.LensModel; 1101 | } 1102 | else 1103 | // 4 rationals giving focal and aperture ranges 1104 | if (lensInfo) 1105 | { 1106 | if (dataObj.Lens) 1107 | dataObj.Lens += " "; 1108 | else 1109 | dataObj.Lens = ""; 1110 | 1111 | dataObj.Lens += lensInfo[0]; 1112 | if (lensInfo[1] > 0) 1113 | dataObj.Lens += "-" + lensInfo[1]; 1114 | dataObj.Lens += "mm"; 1115 | 1116 | if (lensInfo[2] > 0) 1117 | { 1118 | dataObj.Lens += " ƒ/" + lensInfo[2]; 1119 | if (lensInfo[3] > 0) 1120 | dataObj.Lens += "-" + lensInfo[3]; 1121 | } 1122 | } 1123 | } 1124 | 1125 | return ntags; 1126 | } 1127 | } 1128 | -------------------------------------------------------------------------------- /binIptc.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * 5 | * This Source Code Form is "Incompatible With Secondary Licenses", as 6 | * defined by the Mozilla Public License, v. 2.0. 7 | */ 8 | 9 | /* 10 | * Interpreter for binary IPTC-NAA data. 11 | */ 12 | 13 | function iptcClass(stringBundle) 14 | { 15 | var fxifUtils = new fxifUtilsClass(); 16 | 17 | const BIM_MARKER = 0x3842494D; // 8BIM segment marker 18 | const UTF8_INDICATOR = "\u001B%G"; // indicates usage of UTF8 in IPTC-NAA strings 19 | 20 | // IPTC tags 21 | const TAG_IPTC_CODEDCHARSET = 0x5A; 22 | const TAG_IPTC_INSTRUCTIONS = 0x28; 23 | const TAG_IPTC_BYLINE = 0x50; 24 | const TAG_IPTC_CITY = 0x5A; 25 | const TAG_IPTC_SUBLOCATION = 0x5C; 26 | const TAG_IPTC_PROVINCESTATE = 0x5F; 27 | const TAG_IPTC_COUNTRYNAME = 0x65; 28 | const TAG_IPTC_HEADLINE = 0x69; 29 | const TAG_IPTC_COPYRIGHT = 0x74; 30 | const TAG_IPTC_CAPTION = 0x78; 31 | const TAG_IPTC_DATECREATED = 0x37; 32 | const TAG_IPTC_TIMECREATED = 0x3C; 33 | 34 | 35 | // Decodes arrays carrying UTF-8 sequences into Unicode strings. 36 | // Filters out illegal bytes with values between 128 and 191, 37 | // but doesn't validate sequences. 38 | function utf8BytesToString(utf8data, offset, num) 39 | { 40 | var s = ""; 41 | var c = c1 = c2 = 0; 42 | 43 | for (var i = offset; i < offset + num;) { 44 | c = utf8data[i]; 45 | if (c <= 127) { 46 | s += String.fromCharCode(c); 47 | i++; 48 | } 49 | else if ((c >= 192) && (c <= 223)) { 50 | c2 = utf8data[i+1]; 51 | s += String.fromCharCode(((c&31) << 6) | (c2&63)); 52 | i += 2; 53 | } 54 | else if ((c >= 224) && (c <= 239)) { 55 | c2 = utf8data[i+1]; 56 | c3 = utf8data[i+2]; 57 | s += String.fromCharCode(((c&15) << 12) | ((c2&63) << 6) | (c3&63)); 58 | i += 3; 59 | } 60 | else if (c >= 240) { 61 | c2 = utf8data[i+1]; 62 | c3 = utf8data[i+2]; 63 | c4 = utf8data[i+3]; 64 | s += String.fromCharCode(((c & 7) << 18) | ((c2&63) << 12) | ((c3&63) << 6) | (c4&63)); 65 | i += 4; 66 | } 67 | else { 68 | i++; 69 | } 70 | } 71 | 72 | return s; 73 | } 74 | 75 | /* Reads the actual IPTC/NAA tags. 76 | Overwrites information from EXIF tags for textual informations like 77 | By, Caption, Headline, Copyright. 78 | But doesn't overwrite those fields when already populated by IPTC4XMP. 79 | The tag CodedCharacterSet in record 1 is read and interpreted to detect 80 | if the string data in record 2 is supposed to be UTF-8 coded. For now 81 | we assume record 1 comes before 2 in the file. 82 | */ 83 | function readIptcDir(dataObj, data) 84 | { 85 | var pos = 0; 86 | var utf8Strings = false; 87 | 88 | // keep until we're through the whole date because 89 | // only then we have both values. 90 | var iptcDate; 91 | var iptcTime; 92 | 93 | // Don't read outside the array, take the 5 bytes into account 94 | // since they are mandatory for a proper entry. 95 | while (pos + 5 <= data.length) { 96 | var entryMarker = data[pos]; 97 | var entryRecord = data[pos + 1]; 98 | var tag = data[pos + 2]; 99 | // dataLen is really only the length of the data. 100 | // There are signs, that the highest bit of this int 101 | // indicates an extended tag. Be aware of this. 102 | var dataLen = fxifUtils.read16(data, pos + 3, false); 103 | if (entryMarker == 0x1C) { 104 | if (entryRecord == 0x01) { 105 | // Only use tags with length > 0, tags without actual data are common. 106 | if (dataLen > 0) { 107 | if (pos + 5 + dataLen > data.length) { // Don't read outside the array. 108 | var read = pos + 5 + dataLen; 109 | alert("Read outside of array, read to: " + read + ", array length: " + data.length); 110 | break; 111 | } 112 | if (tag == TAG_IPTC_CODEDCHARSET) { 113 | var val = fxifUtils.bytesToString(data, pos + 5, dataLen, false, 1); 114 | // ESC %G 115 | if (val == UTF8_INDICATOR) { 116 | utf8Strings = true; 117 | } 118 | } 119 | } 120 | } 121 | else 122 | if (entryRecord == 0x02) { 123 | // Only use tags with length > 0, tags without actual data are common. 124 | if (dataLen > 0) { 125 | if (pos + 5 + dataLen > data.length) { // Don't read outside the array. 126 | var read = pos + 5 + dataLen; 127 | alert("Read outside of array, read to: " + read + ", array length: " + data.length); 128 | break; 129 | } 130 | if (utf8Strings) { 131 | var val = utf8BytesToString(data, pos + 5, dataLen); 132 | } 133 | else { 134 | var val = fxifUtils.bytesToString(data, pos + 5, dataLen, false, 1); 135 | } 136 | switch(tag) { 137 | case TAG_IPTC_DATECREATED: 138 | iptcDate = val; 139 | break; 140 | 141 | case TAG_IPTC_TIMECREATED: 142 | iptcTime = val; 143 | break; 144 | 145 | case TAG_IPTC_BYLINE: 146 | if (!dataObj.Creator || !fxifUtils.xmpDone) 147 | dataObj.Creator = val; 148 | break; 149 | 150 | case TAG_IPTC_CITY: 151 | if (!dataObj.City || !fxifUtils.xmpDone) 152 | dataObj.City = val; 153 | break; 154 | 155 | case TAG_IPTC_SUBLOCATION: 156 | if (!dataObj.Sublocation || !fxifUtils.xmpDone) 157 | dataObj.Sublocation = val; 158 | break; 159 | 160 | case TAG_IPTC_PROVINCESTATE: 161 | if (!dataObj.ProvinceState || !fxifUtils.xmpDone) 162 | dataObj.ProvinceState = val; 163 | break; 164 | 165 | case TAG_IPTC_COUNTRYNAME: 166 | if (!dataObj.CountryName || !fxifUtils.xmpDone) 167 | dataObj.CountryName = val; 168 | break; 169 | 170 | case TAG_IPTC_CAPTION: 171 | if (!dataObj.Caption || !fxifUtils.xmpDone) 172 | dataObj.Caption = val; 173 | break; 174 | 175 | case TAG_IPTC_HEADLINE: 176 | if (!dataObj.Headline || !fxifUtils.xmpDone) 177 | dataObj.Headline = val; 178 | break; 179 | 180 | case TAG_IPTC_COPYRIGHT: 181 | if (!dataObj.Copyright || !fxifUtils.xmpDone) 182 | dataObj.Copyright = val; 183 | break; 184 | 185 | case TAG_IPTC_INSTRUCTIONS: 186 | dataObj.Instructions = val; 187 | break; 188 | } 189 | } 190 | } 191 | else 192 | { 193 | // alert("Tag: " + tag + ", dataLen: " + dataLen); 194 | } 195 | } 196 | else { 197 | // alert("Wrong entryMarker (" + entryMarker + ")"); 198 | break; 199 | } 200 | 201 | pos += 5 + dataLen; 202 | } 203 | 204 | // only overwrite existing date if XMP data not already parsed 205 | if ((!dataObj.Date || !fxifUtils.xmpDone) && (iptcDate || iptcTime)) 206 | { 207 | // if IPTC only contains either date or time, only use it if there’s 208 | // no date already set 209 | if ((iptcDate && iptcTime) || !dataObj.Date && (iptcDate && !iptcTime || !iptcDate && iptcTime)) 210 | { 211 | var date; 212 | var matches; 213 | if (iptcDate) 214 | { 215 | matches = iptcDate.match(/^(\d{4})(\d{2})(\d{2})$/); 216 | if (matches) 217 | date = matches[1] + '-' + matches[2] + '-' + matches[3]; 218 | } 219 | if (iptcTime) 220 | { 221 | matches = iptcTime.match(/^(\d{2})(\d{2})(\d{2})([+-]\d{4})?$/); 222 | if (matches) 223 | { 224 | if (date) 225 | date += ' '; 226 | date += matches[1] + ':' + matches[2] + ':' + matches[3]; 227 | if (matches[4]) 228 | date += ' ' + matches[4]; 229 | else 230 | date += ' ' + stringBundle.getString("noTZ"); 231 | } 232 | } 233 | 234 | dataObj.Date = date; 235 | } 236 | } 237 | } 238 | 239 | 240 | /* Looks for 8BIM markers in this image resources block. 241 | The format is defined by Adobe and stems from its PSD 242 | format. 243 | */ 244 | this.readPsSection = function (dataObj, psData) 245 | { 246 | var pointer = 0; 247 | 248 | var segmentMarker = fxifUtils.read32(psData, pointer, false); 249 | pointer += 4; 250 | while (segmentMarker == BIM_MARKER && 251 | pointer < psData.length) { 252 | var segmentType = fxifUtils.read16(psData, pointer, false); 253 | pointer += 2; 254 | // Step over 8BIM header. 255 | // It's an even length pascal string, i.e. one byte length information 256 | // plus string. The whole thing is padded to have an even length. 257 | var headerLen = psData[pointer]; 258 | headerLen = 1 + headerLen + ((headerLen + 1) % 2); 259 | pointer += headerLen; 260 | 261 | var segmentLen = 0; 262 | if (pointer + 4 <= psData.length) { 263 | // read dir length excluding length field 264 | segmentLen = fxifUtils.read32(psData, pointer, false); 265 | pointer += 4; 266 | } 267 | 268 | // IPTC-NAA record as IIM 269 | if (segmentType == 0x0404 && segmentLen > 0) { 270 | // Check if the next bytes are what we expect. 271 | // I’ve seen files where the segment length field is just missing 272 | // and so we’re bytes to far. 273 | if (pointer + 2 <= psData.length) { 274 | var entryMarker = psData[pointer]; 275 | var entryRecord = psData[pointer + 1]; 276 | if (entryMarker != 0x1C || entryRecord >= 0x0F) { 277 | // Go back 4 bytes since we can’t be sure this header is ok. 278 | pointer -= 4; 279 | 280 | // Something’s wrong. Try to recover by searching 281 | // the last bytes for the expect markers. 282 | var i = 0; 283 | while (i < 4) { // find first tag 284 | if (psData[pointer + i] == 0x1C && psData[pointer + i + 1] < 0x0F) 285 | break; 286 | else 287 | i++; 288 | } 289 | if (i < 4) // found 290 | { 291 | // calculate segmentLen since that’s the field missing 292 | segmentLen = psData.length - (4 + 2 + headerLen + i); 293 | pointer += i; 294 | } 295 | else 296 | throw "No entry marker found."; 297 | } 298 | 299 | readIptcDir(dataObj, psData.slice(pointer, pointer + segmentLen)); 300 | break; 301 | } 302 | } 303 | 304 | // Dir data, variable length padded to even length. 305 | pointer += segmentLen + (segmentLen % 2); 306 | segmentMarker = fxifUtils.read32(psData, pointer, false); 307 | pointer += 4; 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /contentscript.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * 5 | * This Source Code Form is "Incompatible With Secondary Licenses", as 6 | * defined by the Mozilla Public License, v. 2.0. 7 | */ 8 | 9 | function addByteStreamIF(arr) { 10 | arr.bisOffset = 0; 11 | arr.read16 = () => { 12 | var val = arr[arr.bisOffset] << 8 | arr[arr.bisOffset + 1]; 13 | arr.bisOffset += 2; 14 | return val; 15 | } 16 | arr.readBytes = byteCount => { 17 | var retval = ""; 18 | for (var i = 0; i < byteCount; i++) { 19 | retval += String.fromCharCode(arr[arr.bisOffset]); 20 | arr.bisOffset++; 21 | } 22 | return retval; 23 | } 24 | arr.readByteArray = len => { 25 | var retval = arr.subarray(arr.bisOffset, arr.bisOffset + len); 26 | arr.bisOffset += len; 27 | return retval; 28 | } 29 | } 30 | 31 | function translateFields(data) { 32 | var newdata = {}; 33 | Object.keys(data).forEach(key_v => { 34 | var key = key_v; 35 | var val = data[key_v]; 36 | if (typeof key == "string") { 37 | key = stringBundle.getString(key); 38 | } 39 | if (typeof val == "string") { 40 | val = stringBundle.getString(val); 41 | } 42 | newdata[key] = val; 43 | }); 44 | 45 | return newdata; 46 | } 47 | 48 | if (typeof contentListenerAdded === 'undefined') { 49 | browser.runtime.onMessage.addListener(request => { 50 | if (request.message == "parseImage" && 51 | typeof request.imageURL !== 'undefined') { 52 | var xhr = new XMLHttpRequest(); 53 | xhr.open("GET", request.imageURL, true); 54 | xhr.responseType = "arraybuffer"; 55 | xhr.addEventListener("load", () => { 56 | var arrayBuffer = xhr.response; 57 | if (arrayBuffer) { 58 | var byteArray = new Uint8Array(arrayBuffer); 59 | addByteStreamIF(byteArray); 60 | var dataObj = fxifObj.gatherData(byteArray); 61 | xlatData = translateFields(dataObj); 62 | browser.runtime.sendMessage({ 63 | message: "EXIFready", 64 | data: xlatData 65 | }); 66 | } 67 | }); 68 | 69 | xhr.addEventListener("error", () => { 70 | console.log("wxIF xhr error:" + xhr.statusText); 71 | }); 72 | 73 | xhr.send(); 74 | } 75 | }); 76 | } 77 | 78 | var contentListenerAdded = true; 79 | -------------------------------------------------------------------------------- /fxifUtils.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * 5 | * This Source Code Form is "Incompatible With Secondary Licenses", as 6 | * defined by the Mozilla Public License, v. 2.0. 7 | */ 8 | 9 | "use strict"; 10 | 11 | /* 12 | * Some utility functions for FxIF. 13 | */ 14 | 15 | function fxifUtilsClass () 16 | { 17 | var prefInstance = null; 18 | 19 | this.exifDone = false; 20 | this.iptcDone = false; 21 | this.xmpDone = false; 22 | 23 | this.read16 = function (data, offset, swapbytes) 24 | { 25 | if(!swapbytes) 26 | return (data[offset] << 8) | data[offset+1]; 27 | 28 | return data[offset] | (data[offset+1] << 8); 29 | } 30 | 31 | this.read32 = function (data, offset, swapbytes) 32 | { 33 | if (!swapbytes) 34 | return (data[offset] << 24) | (data[offset+1] << 16) | (data[offset+2] << 8) | data[offset+3]; 35 | 36 | return data[offset] | (data[offset+1] << 8) | (data[offset+2] << 16) | (data[offset+3] << 24); 37 | } 38 | 39 | /* charWidth should normally be 1 and this function thus reads 40 | * the bytes one by one. But reading Unicode needs reading 41 | * 16 Bit values. 42 | * Stops at the first null byte. 43 | */ 44 | this.bytesToString = function (data, offset, num, swapbytes, charWidth) 45 | { 46 | var s = ""; 47 | 48 | if (charWidth == 1) 49 | { 50 | for (var i = offset; i < offset + num; i++) 51 | { 52 | var charval = data[i]; 53 | if (charval == 0) 54 | break; 55 | 56 | s += String.fromCharCode(charval); 57 | } 58 | } 59 | else 60 | { 61 | for (var i = offset; i < offset + num; i += 2) 62 | { 63 | var charval = this.read16(data, i, swapbytes); 64 | if (charval == 0) 65 | break; 66 | 67 | s += String.fromCharCode(charval); 68 | } 69 | } 70 | 71 | return s; 72 | } 73 | 74 | /* Doesn’t stop at null bytes. */ 75 | this.bytesToStringWithNull = function (data, offset, num) 76 | { 77 | var s = ""; 78 | 79 | for (var i = offset; i < offset + num; i++) 80 | s += String.fromCharCode(data[i]); 81 | 82 | return s; 83 | } 84 | 85 | this.dd2dms = function (gpsval) 86 | { 87 | // a bit unconventional calculation to get input edge cases 88 | // like 0x31 / 0x01, 0x0a / 0x01, 0x3c / 0x01 to 49°11'0" instead of 49°10'60" 89 | var gpsDeg = Math.floor(gpsval / 3600); 90 | gpsval -= gpsDeg * 3600.0; 91 | var gpsMin = Math.floor(gpsval / 60); 92 | // round to 2 digits after the comma 93 | var gpsSec = (gpsval - gpsMin * 60.0).toFixed(2); 94 | return new Array(gpsDeg, gpsMin, gpsSec); 95 | } 96 | 97 | this.dd2dm = function (gpsval) 98 | { 99 | // a bit unconventional calculation to get input edge cases 100 | // like 0x31 / 0x01, 0x0a / 0x01, 0x3c / 0x01 to 49°11'0" instead of 49°10'60" 101 | var gpsDeg = Math.floor(gpsval / 3600); 102 | gpsval -= gpsDeg * 3600.0; 103 | // round to 2 digits after the comma 104 | var gpsMin = (gpsval / 60).toFixed(4); 105 | return new Array(gpsDeg, gpsMin); 106 | } 107 | 108 | this.dd2dd = function (gpsval) 109 | { 110 | // round to 6 digits after the comma 111 | var gpsArr = new Array(); 112 | gpsArr.push((gpsval / 3600).toFixed(6)); 113 | return gpsArr; 114 | } 115 | 116 | this.getPreferences = function () 117 | { 118 | // console.log("getPreferences"); 119 | } 120 | 121 | // Retrieves the language which is likely to be the users favourite one. 122 | // Currently we end up using only the first language code. 123 | this.getLang = function () 124 | { 125 | return browser.i18n.getUILanguage(); 126 | } 127 | } 128 | 129 | var fxifUtils = new fxifUtilsClass(); -------------------------------------------------------------------------------- /icons/fxif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcp/wxif/21c3f5c095d1db972f26fb048ed4a560858d238b/icons/fxif.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "manifest_version": 2, 4 | "name": "wxIF", 5 | "version": "0.2", 6 | "default_locale": "en", 7 | 8 | "description": "__MSG_extensionDescription__", 9 | "homepage_url": "https://github.com/gcp/wxif", 10 | 11 | "icons": { 12 | "16": "icons/fxif.png", 13 | "32": "icons/fxif.png", 14 | "48": "icons/fxif.png", 15 | "96": "icons/fxif.png" 16 | }, 17 | 18 | "permissions": [ 19 | "", 20 | "contextMenus", 21 | "activeTab" 22 | ], 23 | 24 | "background": { 25 | "scripts": ["backgroundscript.js"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /parseJpeg.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * 5 | * This Source Code Form is "Incompatible With Secondary Licenses", as 6 | * defined by the Mozilla Public License, v. 2.0. 7 | */ 8 | 9 | function fxifClass() { 10 | const SOI_MARKER = 0xFFD8; // start of image 11 | const SOS_MARKER = 0xFFDA; // start of stream 12 | const EOI_MARKER = 0xFFD9; // end of image 13 | const APP1_MARKER = 0xFFE1; // start of binary EXIF data 14 | const APP13_MARKER = 0xFFED; // start of IPTC-NAA data 15 | const COM_MARKER = 0xFFFE; // start of JFIF comment data 16 | 17 | const INTEL_BYTE_ORDER = 0x4949; 18 | 19 | this.gatherData = bis => { 20 | var dataObj = {}; 21 | var swapbytes = false; 22 | var marker = bis.read16(); 23 | var len; 24 | 25 | if (marker == SOI_MARKER) { 26 | marker = bis.read16(); 27 | // reading SOS marker indicates start of image stream 28 | while (marker != SOS_MARKER && 29 | (!fxifUtils.exifDone || !fxifUtils.iptcDone || !fxifUtils.xmpDone)) { 30 | // length includes the length bytes 31 | len = bis.read16() - 2; 32 | 33 | if (marker == APP1_MARKER && len >= 6) { 34 | // for EXIF the first 6 bytes should be 'Exif\0\0' 35 | var header = bis.readBytes(6); 36 | // Is it EXIF? 37 | if (header == 'Exif\0\0') { 38 | // 8 byte TIFF header 39 | // first two determine byte order 40 | var exifData = bis.readByteArray(len - 6); 41 | 42 | swapbytes = fxifUtils.read16(exifData, 0, false) == INTEL_BYTE_ORDER; 43 | 44 | // next two bytes are always 0x002A 45 | // offset to Image File Directory (includes the previous 8 bytes) 46 | var ifd_ofs = fxifUtils.read32(exifData, 4, swapbytes); 47 | var exifReader = new exifClass(); 48 | try { 49 | exifReader.readExifDir(dataObj, exifData, ifd_ofs, swapbytes); 50 | } catch (ex) { 51 | pushError(dataObj, "EXIF", ex); 52 | } 53 | fxifUtils.exifDone = true; 54 | } else { 55 | if (len > 28) { 56 | // Maybe it's XMP. If it is, it starts with the XMP namespace URI 57 | // 'http://ns.adobe.com/xap/1.0/\0'. 58 | // see http://partners.adobe.com/public/developer/en/xmp/sdk/XMPspecification.pdf 59 | header += bis.readBytes(22); // 6 bytes read means 22 more to go 60 | if (header == 'http://ns.adobe.com/xap/1.0/') { 61 | // There is at least one programm which writes spaces behind the namespace URI. 62 | // Overread up to 5 bytes of such garbage until a '\0'. I deliberately don't read 63 | // until reaching len bytes. 64 | var a; 65 | var j = 0; 66 | do { 67 | a = bis.readBytes(1); 68 | } while (++j < 5 && a == ' '); 69 | if (a == '\0') { 70 | var xmpData = bis.readByteArray(len - (28 + j)); 71 | try { 72 | var xmpReader = new xmpClass(); 73 | xmpReader.parseXML(dataObj, xmpData); 74 | } catch (ex) { 75 | pushError(dataObj, "XMP", ex); 76 | } 77 | fxifUtils.xmpDone = true; 78 | } else 79 | bis.readBytes(len - (28 + j)); 80 | } else 81 | bis.readBytes(len - 28); 82 | } else { 83 | bis.readBytes(len - 6); 84 | } 85 | } 86 | } else 87 | // Or is it IPTC-NAA record as IIM? 88 | if (marker == APP13_MARKER && len > 14) { 89 | // 6 bytes, 'Photoshop 3.0\0' 90 | var psString = bis.readBytes(14); 91 | var psData = bis.readByteArray(len - 14); 92 | if (psString == 'Photoshop 3.0\0') { 93 | var iptcReader = new iptcClass(stringBundle); 94 | try { 95 | iptcReader.readPsSection(dataObj, psData); 96 | } catch (ex) { 97 | pushError(dataObj, "IPTC", ex); 98 | } 99 | fxifUtils.iptcDone = true; 100 | } 101 | } else 102 | // Or perhaps a JFIF comment? 103 | if (marker == COM_MARKER && len >= 1) { 104 | dataObj.UserComment = fxifUtils.bytesToString(bis.readByteArray(len), 0, len, false, 1); 105 | } else { 106 | // read and discard data ... 107 | bis.readBytes(len); 108 | } 109 | 110 | marker = bis.read16(); 111 | } 112 | } 113 | 114 | return dataObj; 115 | } 116 | 117 | function pushError(dataObj, type, message) 118 | { 119 | if (dataObj.error) 120 | dataObj.error += '\n'; 121 | else 122 | dataObj.error = ''; 123 | dataObj.error += stringBundle.getFormattedString("specialError", [type, type]) + ' ' + message; 124 | } 125 | } 126 | 127 | var fxifObj = new fxifClass(); -------------------------------------------------------------------------------- /popup/popup.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: transparent; 3 | box-sizing: border-box; 4 | color: #222426; 5 | cursor: default; 6 | font: caption; 7 | margin: 0; 8 | overflow: auto; 9 | padding: 0; 10 | -moz-appearance: tabpanels; 11 | width: 100%; 12 | height: 100%; 13 | } 14 | 15 | table { 16 | height: 85%; 17 | } 18 | 19 | button { 20 | display: block; 21 | height: auto; 22 | padding: 4px; 23 | margin: 2px; 24 | } 25 | -------------------------------------------------------------------------------- /popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | EXIF data 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /popup/popupscript.js: -------------------------------------------------------------------------------- 1 | browser.runtime.sendMessage({ 2 | message: "popupReady" 3 | }, response => { 4 | var table = document.getElementById("data"); 5 | Object.keys(response).forEach(key_v => { 6 | var row = table.insertRow(-1); 7 | var label = row.insertCell(0); 8 | var value = row.insertCell(1); 9 | if (key_v != "GPSPureDdLat" && key_v != "GPSPureDdLon") { 10 | label.innerText = key_v; 11 | value.innerText = response[key_v]; 12 | } 13 | }); 14 | if (response.GPSPureDdLat && response.GPSPureDdLon) { 15 | var mapbutton = document.createElement("button"); 16 | mapbutton.type = "button"; 17 | mapbutton.innerText = "Locate on OpenStreetMap"; 18 | mapbutton.onclick = () => { 19 | var href = 'https://www.openstreetmap.org/?mlat=%lat%&mlon=%lon%&layers=M'; 20 | href = href.replace(/%lat%/, response.GPSPureDdLat); 21 | href = href.replace(/%lon%/, response.GPSPureDdLon); 22 | href = href.replace(/%lang%/, browser.i18n.getUILanguage()); 23 | window.open(href); 24 | } 25 | var btns = document.getElementById("buttonzone"); 26 | btns.insertBefore(mapbutton, btns.childNodes[0]); 27 | } 28 | document.getElementById("copybutton").onclick = () => { 29 | var range = document.createRange(); 30 | var sel = window.getSelection(); 31 | range.selectNodeContents(table); 32 | sel.addRange(range); 33 | document.execCommand("copy"); 34 | sel.removeAllRanges(); 35 | }; 36 | }); -------------------------------------------------------------------------------- /stringBundle.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * 5 | * This Source Code Form is "Incompatible With Secondary Licenses", as 6 | * defined by the Mozilla Public License, v. 2.0. 7 | */ 8 | 9 | var stringBundle = { 10 | getString(string) { 11 | var translate = browser.i18n.getMessage(string); 12 | // WebExtension docs are lying here 13 | if (translate == "??" || translate == "" || translate == undefined) { 14 | return string; 15 | } else { 16 | return translate; 17 | } 18 | }, 19 | getFormattedString(string, val) { 20 | var xlat = stringBundle.getString(string); 21 | if (xlat != string) { 22 | return val + xlat; 23 | } else { 24 | return string + "=" + val; 25 | } 26 | } 27 | }; -------------------------------------------------------------------------------- /xmp.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * 5 | * This Source Code Form is "Incompatible With Secondary Licenses", as 6 | * defined by the Mozilla Public License, v. 2.0. 7 | */ 8 | 9 | /* 10 | * Interpreter for XML XMP data. 11 | */ 12 | 13 | function xmpClass() 14 | { 15 | var fxifUtils = new fxifUtilsClass(); 16 | 17 | var NodeTypes = { 18 | ELEMENT_NODE:1, ATTRIBUTE_NODE:2, TEXT_NODE:3, CDATA_SECTION_NODE:4, 19 | ENTITY_REFERENCE_NODE:5, ENTITY_NODE:6, PROCESSING_INSTRUCTION_NODE:7, 20 | COMMENT_NODE:8, DOCUMENT_NODE:9, DOCUMENT_TYPE_NODE:10, 21 | DOCUMENT_FRAGMENT_NODE:11, NOTATION_NODE:12 22 | }; 23 | 24 | 25 | // Parses and reads through the XMP document within the file. 26 | this.parseXML = function (dataObj, xml) 27 | { 28 | var parser = new DOMParser(); 29 | // There is at least one programm which includes a null byte at the end of the document. 30 | // The parser doesn't like this, so shorten the length by one byte of the last one is null. 31 | var doclength = xml.length; 32 | if (xml.length > 1 && xml[xml.length - 1] == 0) 33 | doclength--; 34 | var dom = parser.parseFromBuffer(xml, doclength, 'text/xml'); 35 | 36 | if (dom.documentElement.nodeName == 'parsererror') { 37 | // parsererror might have been caused by incorrect encoding of characters. 38 | // XMP documents in JPEG files have been reported with characters as ISO-8859-1 39 | // (while containing an UTF-8 BOM) or even illegal not UTF-8 encoded "BOM" like 40 | // xpacket begin="i»?" which. 41 | // So just go on and try to save the situation converting from a single byte encoding to Unicode. 42 | // I used iso-8859-1 here which will give wrong characters if the source is encoded differently, 43 | // but getting correct characters isn’t the objective here, just to be able reading the document 44 | // somehow. The document is corrupt anyway. 45 | 46 | // XXX: Not usable in a WebExtension 47 | // var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Components.interfaces.nsIScriptableUnicodeConverter); 48 | // converter.charset = 'iso-8859-1'; 49 | // var xmlString = converter.convertFromByteArray(xml, doclength); 50 | // dom = parser.parseFromString(xmlString, 'text/xml'); 51 | 52 | if (dom.documentElement.nodeName == 'parsererror') { 53 | console.error("Error parsing XML"); 54 | // no known remedy, so don’t throw this problem 55 | return; 56 | } 57 | } 58 | 59 | var val; 60 | 61 | // Creators come in an ordered list. Get them all. 62 | val = getXMPOrderedArray(dom, "http://purl.org/dc/elements/1.1/", "creator", ""); 63 | if (val && val.length) { 64 | dataObj.Creator = val.join(", "); 65 | } 66 | 67 | val = getXMPValue(dom, "http://ns.adobe.com/photoshop/1.0/", "City"); 68 | if (val) 69 | dataObj.City = val; 70 | 71 | val = getXMPValue(dom, "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/", "Location"); 72 | if (val) 73 | dataObj.Sublocation = val; 74 | 75 | val = getXMPValue(dom, "http://ns.adobe.com/photoshop/1.0/", "State"); 76 | if (val) 77 | dataObj.ProvinceState = val; 78 | 79 | val = getXMPValue(dom, "http://ns.adobe.com/photoshop/1.0/", "Country"); 80 | if (val) 81 | dataObj.CountryName = val; 82 | 83 | val = getXMPValue(dom, "http://ns.adobe.com/photoshop/1.0/", "Headline"); 84 | if (val) 85 | dataObj.Headline = val; 86 | 87 | val = getXMPValue(dom, "http://ns.adobe.com/photoshop/1.0/", "Instructions"); 88 | if (val) 89 | dataObj.Instructions = val; 90 | 91 | // only use if not already set 92 | if (!dataObj.Software) 93 | { 94 | val = getXMPValue(dom, "http://ns.adobe.com/xap/1.0/", "CreatorTool"); 95 | if (val) 96 | dataObj.Software = val; 97 | } 98 | 99 | val = getXMPOrderedArray(dom, "http://ns.adobe.com/xap/1.0/mm/", "History", "http://ns.adobe.com/xap/1.0/sType/ResourceEvent#", "softwareAgent"); 100 | if(val && val.length) { 101 | dataObj.Software = val[val.length - 1]; 102 | } 103 | 104 | 105 | var lang = fxifUtils.getLang(); 106 | // Build a regular expression to be used to test the language 107 | // alternatives available in the XMP. 108 | var langTest = new RegExp("^"+lang.match(/^[a-z]{2,3}/i), "i") 109 | 110 | if (!dataObj.Headline) 111 | { 112 | val = getXMPAltValue(dom, "http://purl.org/dc/elements/1.1/", "title", langTest); 113 | if(val) { 114 | dataObj.Headline = val; 115 | } 116 | } 117 | 118 | // val = getXMPAltValue(dom, "dc:description", langTest); 119 | val = getXMPAltValue(dom, "http://purl.org/dc/elements/1.1/", "description", langTest); 120 | if(val) { 121 | dataObj.Caption = val; 122 | } 123 | 124 | // val = getXMPAltValue(dom, "dc:rights", langTest); 125 | val = getXMPAltValue(dom, "http://purl.org/dc/elements/1.1/", "rights", langTest); 126 | if(val) 127 | dataObj.Copyright = val; 128 | else 129 | { 130 | val = getXMPAltValue(dom, "http://ns.adobe.com/xap/1.0/rights/", "UsageTerms ", langTest); 131 | if(val) 132 | dataObj.Copyright = val; 133 | else 134 | val = getXMPValue(dom, "http://creativecommons.org/ns#", "license"); 135 | } 136 | if(val) 137 | dataObj.Copyright = val; 138 | 139 | 140 | 141 | // XMP:EXIF 142 | 143 | val = getXMPValue(dom, "http://ns.adobe.com/tiff/1.0/", "Make"); 144 | if (val) 145 | dataObj.Make = val; 146 | 147 | val = getXMPValue(dom, "http://ns.adobe.com/tiff/1.0/", "Model"); 148 | if (val) 149 | dataObj.Model = val; 150 | 151 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/aux/", "Lens"); 152 | if (val) 153 | dataObj.Lens = val; 154 | 155 | if(!dataObj.Date) 156 | { 157 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "DateTimeDigitized"); 158 | if (val) { 159 | var date = readAndFormatISODate(val); 160 | if (date) 161 | dataObj.Date = date; 162 | } 163 | } 164 | 165 | val = getXMPValue(dom, "http://ns.adobe.com/photoshop/1.0/", "CreateDate"); 166 | if (val) { 167 | var date = readAndFormatISODate(val); 168 | if (date) 169 | dataObj.Date = date; 170 | } 171 | 172 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "DateTimeOriginal"); 173 | if (val) { 174 | var date = readAndFormatISODate(val); 175 | if (date) 176 | dataObj.Date = date; 177 | } 178 | 179 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "FNumber"); // expects a rational (eg. "5/1") 180 | if (!val) 181 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "ApertureValue"); // expects a rational (eg. "4643856/1000000") 182 | if (val) 183 | { 184 | try 185 | { 186 | dataObj.ApertureFNumber = "ƒ/" + parseRational(val).toFixed(1); 187 | } 188 | catch (ex) 189 | { 190 | if (!dataObj.ApertureFNumber) 191 | dataObj.ApertureFNumber = val; 192 | } 193 | } 194 | 195 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "FocalLength"); // expects a rational (eg. "2000/10") 196 | if (val) 197 | try 198 | { 199 | dataObj.FocalLength = parseRational(val).toFixed(1); 200 | } 201 | catch (ex) 202 | { 203 | if (!dataObj.FocalLength) 204 | dataObj.FocalLength = val; 205 | } 206 | 207 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "FocalLengthIn35mmFilm"); 208 | if (!val) 209 | // this name is no official one, but written by some applications 210 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "FocalLengthIn35mmFormat"); 211 | if (val) 212 | dataObj.FocalLength35mmEquiv = val; 213 | 214 | if (dataObj.FocalLength) { 215 | var fl = stringBundle.getFormattedString("millimeters", [dataObj.FocalLength]); 216 | if (dataObj.FocalLength35mmEquiv) { 217 | dataObj.FocalLength35mmEquiv = parseFloat(dataObj.FocalLength35mmEquiv); 218 | fl += " " + stringBundle.getFormattedString("35mmequiv", [dataObj.FocalLength35mmEquiv.toFixed(0)]); 219 | } 220 | 221 | dataObj.FocalLengthText = fl; 222 | } 223 | 224 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "SubjectDistance"); 225 | if (val) 226 | { 227 | try 228 | { 229 | var distance = parseRational(val).toFixed(2); 230 | if (distance < 0) 231 | dataObj.Distance = stringBundle.getString("infinite"); 232 | else 233 | dataObj.Distance = stringBundle.getFormattedString("meters", [distance]); 234 | } 235 | catch (ex) 236 | { 237 | if (!dataObj.Distance) 238 | dataObj.Distance = val; 239 | } 240 | } 241 | 242 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "ExposureTime"); 243 | if (val) 244 | { 245 | try 246 | { 247 | var et = ""; 248 | val = parseRational(val); 249 | if (val < 0.010) 250 | et = stringBundle.getFormattedString("seconds", [val.toFixed(4)]); 251 | else 252 | et = stringBundle.getFormattedString("seconds", [val.toFixed(3)]); 253 | if (val <= 0.5) 254 | et += " (1/" + Math.floor(0.5 + 1/val).toFixed(0) + ")"; 255 | dataObj.ExposureTime = et; 256 | } 257 | catch (ex) 258 | { 259 | if (!dataObj.ExposureTime) 260 | dataObj.ExposureTime = val; 261 | } 262 | } 263 | 264 | var el = dom.getElementsByTagNameNS("http://ns.adobe.com/exif/1.0/", "Flash"); 265 | var flashFired = 0; 266 | var flashFunction = 0; 267 | var flashMode = 0; 268 | var redEyeMode = 0; 269 | var flashReturn = 0; 270 | if (el.length) { 271 | // Flash values can occur in two ways, as attribute values of flash 272 | // like 273 | // and as child values, e.g. True 274 | // We've to deal with both and also mixed as a bonus. 275 | flashFired = getSubvalues(el[0], "http://ns.adobe.com/exif/1.0/", "Fired"); 276 | if (!flashFired) 277 | flashFired = el[0].getAttributeNS("http://ns.adobe.com/exif/1.0/", "Fired"); 278 | flashFunction = getSubvalues(el[0], "http://ns.adobe.com/exif/1.0/", "Function"); 279 | if (!flashFunction) 280 | flashFunction = el[0].getAttributeNS("http://ns.adobe.com/exif/1.0/", "Function"); 281 | flashMode = Number(getSubvalues(el[0], "http://ns.adobe.com/exif/1.0/", "Mode")); 282 | if (!flashMode) 283 | flashMode = Number(el[0].getAttributeNS("http://ns.adobe.com/exif/1.0/", "Mode")); 284 | redEyeMode = getSubvalues(el[0], "http://ns.adobe.com/exif/1.0/", "RedEyeMode"); 285 | if (!redEyeMode) 286 | redEyeMode = el[0].getAttributeNS("http://ns.adobe.com/exif/1.0/", "RedEyeMode"); 287 | flashReturn = Number(getSubvalues(el[0], "http://ns.adobe.com/exif/1.0/", "Return")); 288 | if (!flashReturn) 289 | flashReturn = Number(el[0].getAttributeNS("http://ns.adobe.com/exif/1.0/", "Return")); 290 | 291 | var fu; 292 | var addfunc = new Array(); 293 | if (flashFired && flashFired.match(/^true$/i)) { 294 | fu = stringBundle.getString("yes"); 295 | 296 | if (flashMode == 3) 297 | addfunc.push(stringBundle.getString("auto")); 298 | else 299 | if (flashMode == 1) 300 | addfunc.push(stringBundle.getString("enforced")); 301 | 302 | if (redEyeMode && redEyeMode.match(/^true$/i)) 303 | addfunc.push(stringBundle.getString("redeye")); 304 | 305 | if (flashReturn == 3) 306 | addfunc.push(stringBundle.getString("returnlight")); 307 | else 308 | if (flashReturn == 2) 309 | addfunc.push(stringBundle.getString("noreturnlight")); 310 | } 311 | else { 312 | fu = stringBundle.getString("no"); 313 | if (flashFunction && flashFunction.match(/^true$/i)) 314 | addfunc.push(stringBundle.getString("noflash")); 315 | else 316 | if (flashMode == 2) 317 | addfunc.push(stringBundle.getString("enforced")); 318 | else 319 | if (flashMode == 3) 320 | addfunc.push(stringBundle.getString("auto")); 321 | } 322 | 323 | if (addfunc.length) 324 | fu += " (" + addfunc.join(", ") + ")"; 325 | 326 | dataObj.FlashUsed = fu; 327 | } 328 | 329 | val = getXMPValue(dom, "http://ns.adobe.com/tiff/1.0/", "Orientation"); 330 | if (!dataObj.Orientation && val && val > 0) { 331 | if (val <= 8) 332 | dataObj.Orientation = stringBundle.getString("orientation" + val); 333 | else 334 | dataObj.Orientation = stringBundle.getString("unknown") + " (" + val + ")"; 335 | } 336 | 337 | val = getXMPValue(dom, "http://ns.adobe.com/tiff/1.0/", "ImageHeight"); 338 | if (val) 339 | dataObj.Length = val; 340 | 341 | val = getXMPValue(dom, "http://ns.adobe.com/tiff/1.0/", "ImageWidth"); 342 | if (val) 343 | dataObj.Width = val; 344 | 345 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "FocalPlaneXResolution"); 346 | if (val) 347 | dataObj.FocalPlaneXRes = val; 348 | 349 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "FocalPlaneResolutionUnit"); 350 | if (val) { 351 | if (val.search(/^\d+$/) != -1) // Unfortunately there are applications which write non-number values 352 | { 353 | switch(Number(val)) { 354 | case 1: dataObj.FocalPlaneUnits = 25.4; break; // inches 355 | case 2: 356 | // According to the information I was using, 2 means meters. 357 | // But looking at the Canon powershot's files, inches is the only 358 | // sensible value. 359 | dataObj.FocalPlaneUnits = 25.4; 360 | break; 361 | case 3: dataObj.FocalPlaneUnits = 10; break; // centimeter 362 | case 4: dataObj.FocalPlaneUnits = 1; break; // millimeter 363 | case 5: dataObj.FocalPlaneUnits = .001; break; // micrometer 364 | } 365 | } 366 | else 367 | if (!dataObj.FocalPlaneUnits) 368 | dataObj.FocalPlaneUnits = val; 369 | } 370 | 371 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "ExposureBiasValue"); 372 | if (val) { 373 | try 374 | { 375 | val = parseRational(val).toFixed(2); 376 | if (val == 0) 377 | dataObj.ExposureBias = stringBundle.getString("none"); 378 | else 379 | // add a + sign before positive values 380 | dataObj.ExposureBias = (val > 0 ? '+' : '') + stringBundle.getFormattedString("ev", [val]); 381 | } 382 | catch (ex) 383 | { 384 | if (!dataObj.ExposureBias) 385 | dataObj.ExposureBias = val; 386 | } 387 | } 388 | 389 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "WhiteBalance"); 390 | if (val) { 391 | if (val.search(/^\d+$/) != -1) // Unfortunately there are applications which write non-number values 392 | { 393 | switch(Number(val)) { 394 | case 0: 395 | dataObj.WhiteBalance = stringBundle.getString("auto"); 396 | break; 397 | case 1: 398 | dataObj.WhiteBalance = stringBundle.getString("manual"); 399 | break; 400 | } 401 | } 402 | else 403 | if (!dataObj.WhiteBalance) 404 | dataObj.WhiteBalance = val; 405 | } 406 | 407 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "LightSource"); 408 | if (val) { 409 | if (val.search(/^\d+$/) != -1) // Unfortunately there are applications which write non-number values 410 | { 411 | switch(Number(val)) { 412 | case 0: 413 | dataObj.LightSource = stringBundle.getString("unknown"); 414 | break; 415 | case 1: 416 | dataObj.LightSource = stringBundle.getString("daylight"); 417 | break; 418 | case 2: 419 | dataObj.LightSource = stringBundle.getString("fluorescent"); 420 | break; 421 | case 3: 422 | dataObj.LightSource = stringBundle.getString("incandescent"); 423 | break; 424 | case 4: 425 | dataObj.LightSource = stringBundle.getString("flash"); 426 | break; 427 | case 9: 428 | dataObj.LightSource = stringBundle.getString("fineweather"); 429 | break; 430 | case 10: 431 | dataObj.LightSource = stringBundle.getString("cloudy"); 432 | break; 433 | case 11: 434 | dataObj.LightSource = stringBundle.getString("shade"); 435 | break; 436 | case 12: 437 | dataObj.LightSource = stringBundle.getString("daylightfluorescent"); 438 | break; 439 | case 13: 440 | dataObj.LightSource = stringBundle.getString("daywhitefluorescent"); 441 | break; 442 | case 14: 443 | dataObj.LightSource = stringBundle.getString("coolwhitefluorescent"); 444 | break; 445 | case 15: 446 | dataObj.LightSource = stringBundle.getString("whitefluorescent"); 447 | break; 448 | case 24: 449 | dataObj.LightSource = stringBundle.getString("studiotungsten"); 450 | break; 451 | default:; //Quercus: 17-1-2004 There are many more modes for this, check Exif2.2 specs 452 | // If it just says 'unknown' or we don't know it, then 453 | // don't bother showing it - it doesn't add any useful information. 454 | } 455 | } 456 | else 457 | if (!dataObj.LightSource) 458 | dataObj.LightSource = val; 459 | } 460 | 461 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "MeteringMode"); 462 | if (val) { 463 | if (val.search(/^\d+$/) != -1) // Unfortunately there are applications which write non-number values 464 | { 465 | switch(Number(val)) { 466 | case 0: 467 | dataObj.MeteringMode = stringBundle.getString("unknown"); 468 | break; 469 | case 1: 470 | dataObj.MeteringMode = stringBundle.getString("average"); 471 | break; 472 | case 2: 473 | dataObj.MeteringMode = stringBundle.getString("centerweight"); 474 | break; 475 | case 3: 476 | dataObj.MeteringMode = stringBundle.getString("spot"); 477 | break; 478 | case 3: 479 | dataObj.MeteringMode = stringBundle.getString("multispot"); 480 | break; 481 | case 5: 482 | dataObj.MeteringMode = stringBundle.getString("matrix"); 483 | break; 484 | case 6: 485 | dataObj.MeteringMode = stringBundle.getString("partial"); 486 | break; 487 | } 488 | } 489 | else 490 | if (!dataObj.MeteringMode) 491 | dataObj.MeteringMode = val; 492 | } 493 | 494 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "ExposureProgram"); 495 | if (val) { 496 | if (val.search(/^\d+$/) != -1) // Unfortunately there are applications which write non-number values 497 | { 498 | switch(Number(val)) { 499 | case 1: 500 | dataObj.ExposureProgram = stringBundle.getString("manual"); 501 | break; 502 | case 2: 503 | dataObj.ExposureProgram = stringBundle.getString("program") + " (" 504 | + stringBundle.getString("auto") + ")"; 505 | break; 506 | case 3: 507 | dataObj.ExposureProgram = stringBundle.getString("apriority") 508 | + " (" + stringBundle.getString("semiauto") + ")"; 509 | break; 510 | case 4: 511 | dataObj.ExposureProgram = stringBundle.getString("spriority") 512 | + " (" + stringBundle.getString("semiauto") +")"; 513 | break; 514 | case 5: 515 | dataObj.ExposureProgram = stringBundle.getString("creative"); 516 | break; 517 | case 6: 518 | dataObj.ExposureProgram = stringBundle.getString("action"); 519 | break; 520 | case 7: 521 | dataObj.ExposureProgram = stringBundle.getString("portrait"); 522 | break; 523 | case 8: 524 | dataObj.ExposureProgram = stringBundle.getString("landscape"); 525 | break; 526 | default: 527 | break; 528 | } 529 | } 530 | else 531 | if (!dataObj.ExposureProgram) 532 | dataObj.ExposureProgram = val; 533 | } 534 | 535 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "ExposureIndex"); 536 | if (val) { 537 | if (!dataObj.ExposureIndex) 538 | try 539 | { 540 | dataObj.ExposureIndex = parseRational(val).toFixed(0); 541 | } 542 | catch (ex) 543 | { 544 | dataObj.ExposureIndex = val; 545 | } 546 | } 547 | 548 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "ExposureMode"); 549 | if (val) { 550 | if (val.search(/^\d+$/) != -1) // Unfortunately there are applications which write non-number values 551 | { 552 | switch(Number(val)) { 553 | case 0: // Automatic 554 | break; 555 | case 1: 556 | dataObj.ExposureMode = stringBundle.getString("manual"); 557 | break; 558 | case 2: 559 | dataObj.ExposureMode = stringBundle.getString("autobracketing"); 560 | break; 561 | } 562 | } 563 | else 564 | if (!dataObj.ExposureProgram) 565 | dataObj.ExposureProgram = val; 566 | } 567 | 568 | val = getXMPOrderedArray(dom, "http://ns.adobe.com/exif/1.0/", "ISOSpeedRatings"); 569 | if(val && val.length) 570 | dataObj.ISOequivalent = val.join(", "); 571 | 572 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "DigitalZoomRatio"); 573 | if (val) 574 | try 575 | { 576 | var floatVal = parseRational(val); 577 | if (floatVal > 1) 578 | dataObj.DigitalZoomRatio = floatVal.toFixed(3) + "x"; 579 | } 580 | catch (ex) 581 | { 582 | if (!dataObj.DigitalZoomRatio) 583 | dataObj.DigitalZoomRatio = val; 584 | } 585 | 586 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "ColorSpace"); 587 | if (val) 588 | { 589 | if(val == 1) 590 | dataObj.ColorSpace = "sRGB"; 591 | else if(val == 2) 592 | dataObj.ColorSpace = "Adobe RGB"; 593 | } 594 | 595 | if (!dataObj.ColorSpace) 596 | { 597 | // At least Photoshop writes ColorSpace "uncalibrated" though it uses 598 | // a defined colorspace which is documented in ICCProfile 599 | val = getXMPValue(dom, "http://ns.adobe.com/photoshop/1.0/", "ICCProfile"); 600 | if(val) 601 | dataObj.ColorSpace = val; 602 | } 603 | 604 | // GPS stuff 605 | 606 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "GPSAltitude"); 607 | var gpsAlt; 608 | if (val) 609 | gpsAlt = parseRational(val); 610 | 611 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "GPSAltitudeRef"); 612 | var gpsAltRef = 0; 613 | if (val) 614 | gpsAltRef = Number(val); 615 | 616 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "GPSImgDirection"); 617 | var gpsImgDir; 618 | if (val) 619 | gpsImgDir = parseRational(val); 620 | 621 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "GPSImgDirectionRef"); 622 | var gpsImgDirRef = 'M'; 623 | if (val) 624 | gpsImgDirRef = val; 625 | 626 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "GPSLatitude"); 627 | var gpsLat; 628 | if (val) 629 | gpsLat = parseGPSPos(val); 630 | 631 | val = getXMPValue(dom, "http://ns.adobe.com/exif/1.0/", "GPSLongitude"); 632 | var gpsLon; 633 | if (val) 634 | gpsLon = parseGPSPos(val); 635 | 636 | 637 | // use dms format by default 638 | var degFormat = "dms"; 639 | var degFormatter = fxifUtils.dd2dms; 640 | try { 641 | // 0 = DMS, 1 = DD, 2 = DM 642 | if (fxifUtils.getPreferences().getIntPref("gpsFormat") == 1) 643 | { 644 | // but dd if the user wants that 645 | degFormat = "dd"; 646 | degFormatter = fxifUtils.dd2dd; 647 | } 648 | else if (fxifUtils.getPreferences().getIntPref("gpsFormat") == 2) 649 | { 650 | // but dd if the user wants that 651 | degFormat = "dm"; 652 | degFormatter = fxifUtils.dd2dm; 653 | } 654 | } catch(e){} 655 | 656 | if (gpsLat != undefined) { 657 | var gpsArr = degFormatter(Math.abs(gpsLat)); 658 | gpsArr.push(gpsLat < 0 ? 'S' : 'N'); 659 | dataObj.GPSLat = stringBundle.getFormattedString("latlon"+degFormat, gpsArr); 660 | } 661 | if (gpsLon != undefined) { 662 | var gpsArr = degFormatter(Math.abs(gpsLon)); 663 | gpsArr.push(gpsLon < 0 ? 'W' : 'E'); 664 | dataObj.GPSLon = stringBundle.getFormattedString("latlon"+degFormat, gpsArr); 665 | } 666 | if (gpsAlt != undefined) { 667 | dataObj.GPSAlt = stringBundle.getFormattedString("meters", [gpsAlt * (gpsAltRef ? -1.0 : 1.0)]); 668 | } 669 | if (gpsImgDir != undefined && (gpsImgDirRef == 'M' || gpsImgDirRef == 'T')) { 670 | dataObj.GPSImgDir = stringBundle.getFormattedString("dir"+gpsImgDirRef, [gpsImgDir]); 671 | } 672 | 673 | // Get the straight decimal values without rounding. 674 | // For creating links to map services. 675 | if (gpsLat != undefined && 676 | gpsLon != undefined) { 677 | dataObj.GPSPureDdLat = gpsLat / 3600; 678 | dataObj.GPSPureDdLon = gpsLon / 3600; 679 | } 680 | } 681 | 682 | // Parse a GPS datum. 683 | // It's stored like 49,9.8672N 684 | function parseGPSPos(gpsstring) 685 | { 686 | var matches = gpsstring.match(/^(\d{1,3}).([0-9.]+) ?([NSEW])$/); 687 | if (matches) 688 | { 689 | var val = matches[1] * 3600 + matches[2] * 60; 690 | val = val * (matches[3] == 'N' || matches[3] == 'E' ? 1.0 : -1.0); 691 | return val; 692 | } 693 | } 694 | 695 | 696 | // Parse rational numbers. They consist of two 697 | // integers separated by a "/". 698 | // Throws an exception if ratstring contains to rational 699 | // (yes, this happens, e.g. GIMP 2.8.10 writes "f/3,5" for FNumber)? 700 | function parseRational(ratstring) 701 | { 702 | var matches = ratstring.match(/^([+-]?\d+)\/(\d+)$/); 703 | if (matches) 704 | { 705 | var val = matches[1] / matches[2]; 706 | return val; 707 | } 708 | else 709 | throw ("ratstring contains no rational"); 710 | } 711 | 712 | // Since JS can't really parse dates and keep timezone informations, 713 | // I only break the string up and reformat it without any JS functions. 714 | // Input format is YYYY-MM-DD[THH:MM[:SS[.SS]][+/-HH:MM|Z]] and 715 | // Output format is YYYY-MM-DD HH:MM:SS [+/-HH:MM] 716 | // It’s a bit more relaxted than the specification in that 717 | // the time zone information is optional 718 | function readAndFormatISODate(datestring) 719 | { 720 | var exploded_date = datestring.match(/^(\d{4}-\d{2}-\d{2})(?:[T ](\d{2}:\d{2})(?:(:\d{2})(?:\.\d+)?)?([+-]\d{2}:\d{2}|Z)?)?$/); 721 | if (exploded_date) 722 | { 723 | date = exploded_date[1]; 724 | if (typeof exploded_date[2] != 'undefined' && exploded_date[2].length > 0) 725 | { 726 | date += ' ' + exploded_date[2]; 727 | if (typeof exploded_date[3] != 'undefined' && exploded_date[3].length > 0) 728 | date += exploded_date[3]; 729 | if (typeof exploded_date[4] != 'undefined' && exploded_date[4].length > 0) 730 | { 731 | if (exploded_date[4] == 'Z') 732 | date += ' UTC'; 733 | else 734 | date += ' ' + exploded_date[4]; 735 | } 736 | else 737 | date += ' ' + stringBundle.getString("noTZ"); 738 | } 739 | return date; 740 | } 741 | } 742 | 743 | 744 | // Retrieves a property stored somewhere in the XMP data. 745 | // Unfortunately there are at least two common ways a property 746 | // value can be stored. 747 | // 1. As property value of an element 748 | // 2. As content of an element with name of the property 749 | // This function looks for both and returns the first one found. 750 | // ns "http://ns.adobe.com/exif/1.0/" 751 | // property "FNumber" 752 | function getXMPValue(dom, ns, property) 753 | { 754 | var el = dom.getElementsByTagNameNS(ns, property); 755 | if(el.length && el[0].hasChildNodes()) 756 | return el[0].firstChild.nodeValue; 757 | 758 | var list = dom.getElementsByTagNameNS("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "Description"); 759 | var val = ""; 760 | 761 | for(var i = 0; i < list.length; i++) 762 | { 763 | var attr = list[i].getAttributeNS(ns, property); 764 | if(attr) 765 | return attr; 766 | } 767 | } 768 | 769 | // Gets names and descriptions that can be available 770 | // in multiple alternative languages. 771 | // But only those in the first structure with the 772 | // given property name is fetched. 773 | function getXMPAltValue(dom, ns, property, langTest) 774 | { 775 | var val; 776 | 777 | var propertyList = dom.getElementsByTagNameNS(ns, property); 778 | 779 | // go through all the property elements (though there should 780 | // only be one) 781 | for(var i = 0; i < propertyList.length && !val; i++) 782 | { 783 | var entriesList = propertyList[0].getElementsByTagNameNS("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "li"); 784 | 785 | for(var j = 0; j < entriesList.length; j++) 786 | { 787 | // found a non empty entry with fitting language 788 | if(entriesList[j].hasChildNodes() && 789 | langTest.test(entriesList[j].getAttribute("xml:lang"))) { 790 | val = entriesList[j].firstChild.nodeValue; 791 | break; 792 | } 793 | } 794 | } 795 | // our language wasn't found or its entry was empty 796 | for(var i = 0; i < propertyList.length && !val; i++) 797 | { 798 | var entriesList = propertyList[0].getElementsByTagNameNS("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "li"); 799 | 800 | for(var j = 0; j < entriesList.length; j++) 801 | { 802 | // found a non empty entry with fitting language 803 | if(entriesList[j].hasChildNodes() && 804 | entriesList[j].getAttribute("xml:lang") == "x-default") { 805 | val = entriesList[j].firstChild.nodeValue; 806 | break; 807 | } 808 | } 809 | } 810 | 811 | return val; 812 | } 813 | 814 | function getSubvalues(dom, ns, property) 815 | { 816 | var val; 817 | var list = dom.getElementsByTagNameNS(ns, property); 818 | if(list.length) { 819 | if (list[0].hasChildNodes()) { 820 | var fc = list[0].firstChild; 821 | if(fc.nodeType == NodeTypes.TEXT_NODE) 822 | val = fc.nodeValue; 823 | } 824 | } 825 | 826 | return val; 827 | } 828 | 829 | // Get all entries from an ordered array. 830 | // Elements might be straight text nodes or come 831 | // with a property qualifier in a more complex organisation like 832 | // values as properties of the li element. 833 | // Currently the getElementsByTagNameNS() methods are used 834 | // for Firefox 2 (Gecko 1.8) compatibility. These are ugly and 835 | // complicated. Should remove this when Firefox 3 is widespread. 836 | //function getXMPOrderedArray(dom, property) 837 | function getXMPOrderedArray(dom, ns, property, attrNS, attrName) 838 | { 839 | var valarray = new Array(); 840 | 841 | var el = dom.getElementsByTagNameNS(ns, property); 842 | if(el.length) { 843 | var list = el[0].getElementsByTagNameNS("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "li"); 844 | for(var i = 0; i < list.length; i++) { 845 | if (list[i].hasChildNodes()) { 846 | var el; 847 | var tmp = list[i].getElementsByTagNameNS(attrNS, attrName); 848 | if (tmp.length && tmp[0].hasChildNodes()) 849 | el = tmp[0].firstChild; 850 | else 851 | el = list[i].firstChild; 852 | if (el.nodeType == NodeTypes.TEXT_NODE) 853 | valarray.push(el.nodeValue); 854 | } 855 | else 856 | // supposedly one element with values as properties 857 | { 858 | var test = list[i].getAttributeNS(attrNS, attrName); 859 | if (test) 860 | valarray.push(test); 861 | } 862 | } 863 | } 864 | 865 | return valarray; 866 | } 867 | } 868 | --------------------------------------------------------------------------------