diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000..5a89cc5bc4 --- /dev/null +++ b/.clang-format @@ -0,0 +1,24 @@ +--- +# Indentation for all files +ColumnLimit: 120 + +# C++ Options +Language: Cpp +BasedOnStyle: Google + +# Force pointers to the type for C++. +DerivePointerAlignment: false +PointerAlignment: Left + +# Useful for sorting the project inclusions and standard library inclusions separately +IncludeBlocks: Preserve + +# Constructor initializers better formatted in presence of preprocessor conditions (see image.cpp) +BreakConstructorInitializers: AfterColon + +# Do not allow SingleLine statements (to improve coverage statistics) +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false + +... diff --git a/.clang-format.optional b/.clang-format.optional deleted file mode 100644 index cac9cee97c..0000000000 --- a/.clang-format.optional +++ /dev/null @@ -1,28 +0,0 @@ ---- -BasedOnStyle: Google -Language: Cpp -Standard: Cpp03 -TabWidth: 4 -UseTab: Never -ColumnLimit: 120 -NamespaceIndentation: All - -AccessModifierOffset: -4 -ContinuationIndentWidth: 4 -IndentWidth: 4 - -BreakBeforeBraces: Custom -BraceWrapping: - AfterStruct: true - AfterClass: true - AfterFunction: true - AfterControlStatement: false - AfterEnum: true - AfterNamespace: true - -AllowShortFunctionsOnASingleLine: None -AllowShortBlocksOnASingleLine: false -AllowShortIfStatementsOnASingleLine: false -AllowShortLoopsOnASingleLine: false - -... diff --git a/.cmake-format b/.cmake-format new file mode 100644 index 0000000000..d1fe3eadb9 --- /dev/null +++ b/.cmake-format @@ -0,0 +1,17 @@ +with section("format"): + + # How wide to allow formatted cmake files + line_width = 150 + + # How many spaces to tab for indent + tab_size = 2 + + # If true, separate flow control names from their parentheses with a space + separate_ctrl_name_with_space = False + + # If true, separate function names from parentheses with a space + separate_fn_name_with_space = False + + # If a statement is wrapped to more than one line, than dangle the closing + # parenthesis on its own line. + dangle_parens = True diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..620f528bea --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +charset = utf-8 +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace=true diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..e0a57052fa --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# ignore global clang-format +30bf563f4d71ff284b5f42d45f77226200a2e571 +# fic formatting followup from #2158 +a3cb054746beed514679592ffec9378acc9e5d41 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..f5e9a945e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +#### **Describe the bug** +A clear and concise description of the bug, including your use case for exiv2. + +#### **To Reproduce** +Steps to reproduce the behavior: +1. Provide the file with which you observed the issue (commonly named PoC). Explain the status of the file (e.g., direct from camera, edited with darktable), whether you own the copyright and if you allow the file to be added to exiv2 under the [project's license](https://github.com/Exiv2/exiv2/blob/main/COPYING) +2. List the exact commands/functions to reproduce the issue and include any related output +3. Mention the branch and commit in which you observed the issue (e.g., `main, commit 9f721b40`, `0.27-maintenance, latest`) + +#### **Expected behavior** +A short description of what you expected to happen. + +#### **Desktop (please complete the following information):** +- OS and version: (e.g., Linux (Fedora 36), macOS 12.5, Windows 11) +- Exiv2 version and source: (e.g., 0.27.5 from exiv2.org) +- Compiler and version: (e.g., Gcc 12.2, Clang 14.0.0, MSVC 2022) +- Compilation mode and/or compiler flags: (e.g., debug, `-O3`) + +#### **Additional context** +Add any other information about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..bef5acf928 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,25 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: request +assignees: '' + +--- + +#### **Is your feature request related to a problem?** +A clear and concise description of the problem, including your use case for exiv2 and any impact on your workflow. + +#### **Describe the solution you would like** +A short description of what you want to happen. + +#### **Describe alternatives you have considered** +A simple explanation of any alternative solutions (e.g., using ExifTool) or features you have considered (e.g., convert the file to JPEG). + +#### **Desktop** +- OS and version: (e.g., Linux (Fedora 36), macOS 12.5, Windows 11) +- Exiv2 version and source: (e.g., 0.27.5 from exiv2.org) +- Any software using exiv2 and source: (e.g., darktable 4.0 from darktable.org) + +#### **Additional context** +Add any other information about the feature request here (e.g. screenshots, URLs, test files). diff --git a/.github/codeql-queries/exiv2-code-scanning.qls b/.github/codeql-queries/exiv2-code-scanning.qls new file mode 100644 index 0000000000..c371ed848c --- /dev/null +++ b/.github/codeql-queries/exiv2-code-scanning.qls @@ -0,0 +1,3 @@ +# Reusing existing QL Pack +- import: codeql-suites/cpp-code-scanning.qls + from: codeql-cpp diff --git a/.github/codeql-queries/exiv2-cpp-queries/null_iterator_deref.qhelp b/.github/codeql-queries/exiv2-cpp-queries/null_iterator_deref.qhelp new file mode 100644 index 0000000000..3020d2c1e9 --- /dev/null +++ b/.github/codeql-queries/exiv2-cpp-queries/null_iterator_deref.qhelp @@ -0,0 +1,23 @@ + + + +

+ A C++ iterator is a lot like a C pointer: if you dereference it without + first checking that it's valid then it can cause a crash. +

+
+ +

+ Always check that the iterator is valid before derefencing it. +

+
+ +

+ Issue 1763 was caused by + this + dereference of the iterator pos. + The bug was fixed by not dereferencing + pos if pos == metadata->end(). +

+
+
diff --git a/.github/codeql-queries/exiv2-cpp-queries/null_iterator_deref.ql b/.github/codeql-queries/exiv2-cpp-queries/null_iterator_deref.ql new file mode 100644 index 0000000000..9fbcecf5a3 --- /dev/null +++ b/.github/codeql-queries/exiv2-cpp-queries/null_iterator_deref.ql @@ -0,0 +1,47 @@ +/** + * @name NULL iterator deref + * @description Dereferencing an iterator without checking that it's valid could cause a crash. + * @kind problem + * @problem.severity warning + * @id cpp/null-iterator-deref + * @tags security + * external/cwe/cwe-476 + */ + +import cpp +import semmle.code.cpp.controlflow.Guards +import semmle.code.cpp.dataflow.DataFlow + +// Holds if `cond` is a condition like `use == table.end()`. +// `eq_is_true` is `true` for `==`, `false` for '!=`. +// Note: the `==` is actually an overloaded `operator==`. +predicate end_condition(GuardCondition cond, Expr use, FunctionCall endCall, boolean eq_is_true) { + exists(FunctionCall eq | + exists(string opName | eq.getTarget().getName() = opName | + opName = "operator==" and eq_is_true = true + or + opName = "operator!=" and eq_is_true = false + ) and + DataFlow::localExprFlow(use, eq.getAnArgument()) and + DataFlow::localExprFlow(endCall, eq.getAnArgument()) and + endCall.getTarget().getName() = "end" and + DataFlow::localExprFlow(eq, cond) + ) +} + +from FunctionCall call, Expr use +where + call.getTarget().getName() = "findKey" and + DataFlow::localExprFlow(call, use) and + use != call and + not use.(AssignExpr).getRValue() = call and + not end_condition(_, use, _, _) and + not exists( + Expr cond_use, FunctionCall endCall, GuardCondition cond, BasicBlock block, boolean branch + | + end_condition(cond, cond_use, endCall, branch) and + DataFlow::localExprFlow(call, cond_use) and + cond.controls(block, branch.booleanNot()) and + block.contains(use) + ) +select call, "Iterator returned by findKey might cause a null deref $@.", use, "here" diff --git a/.github/codeql-queries/exiv2-cpp-queries/null_metadata_in_print.ql b/.github/codeql-queries/exiv2-cpp-queries/null_metadata_in_print.ql new file mode 100644 index 0000000000..68caed1117 --- /dev/null +++ b/.github/codeql-queries/exiv2-cpp-queries/null_metadata_in_print.ql @@ -0,0 +1,65 @@ +/** + * @name Null metadata in print function + * @description Print functions need to check that the metadata isn't null before calling methods on it. + * @kind problem + * @problem.severity warning + * @id cpp/null-metadata-in-print + */ + +import cpp +import semmle.code.cpp.controlflow.Guards +import semmle.code.cpp.controlflow.Nullness +import semmle.code.cpp.rangeanalysis.RangeAnalysisUtils + +// Find all the print functions by looking for TagInfo initializers +// like this one: +// https://github.com/Exiv2/exiv2/blob/6b186a4cd276ac11b3ea69951c2112f4c4814b9a/src/canonmn_int.cpp#L660-L679 +class PrintFunction extends Function { + PrintFunction() { + exists(Initializer i, Field f | + i.getExpr().(ArrayAggregateLiteral).getAChild().(ClassAggregateLiteral).getAFieldExpr(f) = + this.getAnAccess() and + f.getName() = "printFct_" + ) + } +} + +predicate metadataDeref(Expr metadata) { + exists(Call call | call.getQualifier() = metadata) + or + exists(FunctionCall call, int argIndex, Function f | + call.getArgument(argIndex) = metadata and + f = call.getTarget() and + metadataDeref(f.getParameter(argIndex).getAnAccess()) + ) +} + +predicate unsafePointerParam(Function f, int paramIndex, Expr use) { + exists(Parameter p | + p = f.getParameter(paramIndex) and + use = p.getAnAccess() and + unsafePointerExpr(use) and + not exists(GuardCondition nonNullCheck, BasicBlock block, boolean branch | + validCheckExpr(nonNullCheck, p) and + nonNullCheck.controls(block, branch) and + block.contains(use) + ) + ) +} + +predicate unsafePointerExpr(Expr e) { + exists(Call call | + call.getQualifier() = e and + e.getType().getUnspecifiedType() instanceof PointerType + ) + or + exists(FunctionCall call, int argIndex, Function f | + call.getArgument(argIndex) = e and + f = call.getTarget() and + unsafePointerParam(f, argIndex, _) + ) +} + +from PrintFunction printfcn, Parameter p, Expr metadata +where unsafePointerParam(printfcn, 2, metadata) +select metadata, "Print functions need to check that the metadata isn't null." diff --git a/.github/codeql-queries/exiv2-cpp-queries/qlpack.yml b/.github/codeql-queries/exiv2-cpp-queries/qlpack.yml new file mode 100644 index 0000000000..2a058c5b0e --- /dev/null +++ b/.github/codeql-queries/exiv2-cpp-queries/qlpack.yml @@ -0,0 +1,4 @@ +name: exiv2-cpp-queries +version: 0.0.0 +libraryPathDependencies: codeql-cpp +suites: exiv2-cpp-suite diff --git a/.github/codeql-queries/exiv2-cpp-queries/signed_shift.ql b/.github/codeql-queries/exiv2-cpp-queries/signed_shift.ql new file mode 100644 index 0000000000..a33ec8d29b --- /dev/null +++ b/.github/codeql-queries/exiv2-cpp-queries/signed_shift.ql @@ -0,0 +1,24 @@ +/** + * @name Signed shift + * @description Shifting a negative number is undefined behavior, + * so it is risky to shift a signed number. + * @kind problem + * @problem.severity warning + * @id cpp/signed-shift + * @tags security + * external/cwe/cwe-758 + */ + +// See the "Bitwise shift operators" section here: +// https://en.cppreference.com/w/cpp/language/operator_arithmetic +import cpp +import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis + +from BinaryBitwiseOperation shift, Expr lhs +where + (shift instanceof LShiftExpr or shift instanceof RShiftExpr) and + lhs = shift.getLeftOperand().getFullyConverted() and + lowerBound(lhs) < 0 +select shift, + "This signed shift could cause undefined behavior if the value is negative. Type of lhs: " + + lhs.getType().toString() diff --git a/.github/codeql-queries/exiv2-cpp-queries/unsafe_vector_access.qhelp b/.github/codeql-queries/exiv2-cpp-queries/unsafe_vector_access.qhelp new file mode 100644 index 0000000000..70530983e9 --- /dev/null +++ b/.github/codeql-queries/exiv2-cpp-queries/unsafe_vector_access.qhelp @@ -0,0 +1,30 @@ + + + +

+ The operator[] method of std::vector does not do any bounds checking on the index. It is safer to use the at() method, which does do bounds checking. +

+
+ +

+ Use the at() method, rather than operator[]. +

+

+ Some uses of operator[] are safe because they are protected by a bounds check. The query recognises the following safe coding patterns: +

+ +
+ +

+ #1706 was caused by a lack of bounds-checking on this array access. The bug was fixed calling the at() method instead. +

+
+
diff --git a/.github/codeql-queries/exiv2-cpp-queries/unsafe_vector_access.ql b/.github/codeql-queries/exiv2-cpp-queries/unsafe_vector_access.ql new file mode 100644 index 0000000000..7f6e833590 --- /dev/null +++ b/.github/codeql-queries/exiv2-cpp-queries/unsafe_vector_access.ql @@ -0,0 +1,182 @@ +/** + * @name Unsafe vector access + * @description std::vector::operator[] does not do any runtime + * bounds-checking, so it is safer to use std::vector::at() + * @kind problem + * @problem.severity warning + * @id cpp/unsafe-vector-access + * @tags security + * external/cwe/cwe-125 + */ + +import cpp +import semmle.code.cpp.controlflow.Guards +import semmle.code.cpp.dataflow.DataFlow +import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis +import semmle.code.cpp.rangeanalysis.RangeAnalysisUtils +import semmle.code.cpp.valuenumbering.GlobalValueNumbering +import semmle.code.cpp.valuenumbering.HashCons + +// A call to `operator[]`. +class ArrayIndexCall extends FunctionCall { + ClassTemplateInstantiation ti; + TemplateClass tc; + + ArrayIndexCall() { + this.getTarget().getName() = "operator[]" and + ti = this.getQualifier().getType().getUnspecifiedType() and + tc = ti.getTemplate() and + tc.getSimpleName() != "map" and + tc.getSimpleName() != "match_results" and + tc.getSimpleName() != "unique_ptr" + } + + ClassTemplateInstantiation getClassTemplateInstantiation() { result = ti } + + TemplateClass getTemplateClass() { result = tc } +} + +// A call to `size` or `length`. +class SizeCall extends FunctionCall { + string fname; + + SizeCall() { + fname = this.getTarget().getName() and + (fname = "size" or fname = "length") + } +} + +// `x[i]` is safe if `x` is a `std::array` (fixed-size array) +// and `i` within the bounds of the array. +predicate indexK_with_fixedarray(ClassTemplateInstantiation t, ArrayIndexCall call) { + t = call.getClassTemplateInstantiation() and + exists(Expr idx | + t.getSimpleName() = "array" and + idx = call.getArgument(0) and + lowerBound(idx) >= 0 and + upperBound(idx) < lowerBound(t.getTemplateArgument(1)) + ) +} + +// Holds if `cond` is a Boolean condition that checks the size of +// the array. It handles the following code patterns: +// +// 1. `if (!x.empty()) { ... }` +// 2. `if (x.length()) { ... }` +// 3. `if (x.size() > 2) { ... }` +// 4. `if (x.size() == 2) { ... }` +// 5. `if (x.size() != 0) { ... }` +// +// If it safe to assume that `x.size() >= minsize` on the exit `branch`. +predicate minimum_size_cond(Expr cond, Expr arrayExpr, int minsize, boolean branch) { + // `if (!x.empty()) { ...x[0]... }` + exists(FunctionCall emptyCall | + cond = emptyCall and + arrayExpr = emptyCall.getQualifier() and + emptyCall.getTarget().getName() = "empty" and + minsize = 1 and + branch = false + ) + or + // `if (x.length()) { ...x[0]... }` + exists(SizeCall sizeCall | + cond = sizeCall and + arrayExpr = sizeCall.getQualifier() and + minsize = 1 and + branch = true + ) + or + // `if (x.size() > 2) { ...x[2]... }` + exists(SizeCall sizeCall, Expr k, RelationStrictness strict | + arrayExpr = sizeCall.getQualifier() and + relOpWithSwapAndNegate(cond, sizeCall, k, Greater(), strict, branch) + | + strict = Strict() and minsize = 1 + k.getValue().toInt() + or + strict = Nonstrict() and minsize = k.getValue().toInt() + ) + or + // `if (x.size() == 2) { ...x[1]... }` + exists(SizeCall sizeCall, Expr k | + arrayExpr = sizeCall.getQualifier() and + eqOpWithSwapAndNegate(cond, sizeCall, k, true, branch) and + minsize = k.getValue().toInt() + ) + or + // `if (x.size() != 0) { ...x[0]... }` + exists(SizeCall sizeCall, Expr k | + arrayExpr = sizeCall.getQualifier() and + eqOpWithSwapAndNegate(cond, sizeCall, k, false, branch) and + k.getValue().toInt() = 0 and + minsize = 1 + ) +} + +// Array accesses like these are safe: +// `if (!x.empty()) { ... x[0] ... }` +// `if (x.size() > 2) { ... x[2] ... }` +predicate indexK_with_check(GuardCondition guard, ArrayIndexCall call) { + exists(Expr arrayExpr, BasicBlock block, int i, int minsize, boolean branch | + minimum_size_cond(guard, arrayExpr, minsize, branch) and + ( + globalValueNumber(arrayExpr) = globalValueNumber(call.getQualifier()) or + hashCons(arrayExpr) = hashCons(call.getQualifier()) + ) and + guard.controls(block, branch) and + block.contains(call) and + i = call.getArgument(0).getValue().toInt() and + 0 <= i and + i < minsize + ) +} + +// Array accesses like this are safe: +// `if (i < x.size()) { ... x[i] ... }` +predicate indexI_with_check(GuardCondition guard, ArrayIndexCall call) { + exists(Expr idx, SizeCall sizeCall, BasicBlock block, boolean branch | + relOpWithSwapAndNegate(guard, idx, sizeCall, Lesser(), Strict(), branch) and + ( + globalValueNumber(sizeCall.getQualifier()) = globalValueNumber(call.getQualifier()) and + globalValueNumber(idx) = globalValueNumber(call.getArgument(0)) + or + hashCons(sizeCall.getQualifier()) = hashCons(call.getQualifier()) and + hashCons(idx) = hashCons(call.getArgument(0)) + ) and + guard.controls(block, branch) and + block.contains(call) + ) +} + +// Array accesses like this are safe: +// `if (!x.empty()) { ... x[x.size() - 1] ... }` +predicate index_last_with_check(GuardCondition guard, ArrayIndexCall call) { + exists(Expr arrayExpr, SubExpr minusExpr, SizeCall sizeCall, BasicBlock block, boolean branch | + minimum_size_cond(guard, arrayExpr, _, branch) and + ( + globalValueNumber(arrayExpr) = globalValueNumber(call.getQualifier()) or + hashCons(arrayExpr) = hashCons(call.getQualifier()) + ) and + guard.controls(block, branch) and + block.contains(call) and + minusExpr = call.getArgument(0) and + minusExpr.getRightOperand().getValue().toInt() = 1 and + DataFlow::localExprFlow(sizeCall, minusExpr.getLeftOperand()) and + ( + globalValueNumber(sizeCall.getQualifier()) = globalValueNumber(call.getQualifier()) or + hashCons(sizeCall.getQualifier()) = hashCons(call.getQualifier()) + ) + ) +} + +from ArrayIndexCall call +where + not indexK_with_fixedarray(_, call) and + not indexK_with_check(_, call) and + not indexI_with_check(_, call) and + not index_last_with_check(_, call) and + // Ignore accesses like this: `vsnprintf(&buffer[0], buffer.size(), format, args)` + // That's pointer arithmetic, not a deref, so it's usually a false positive. + not exists(AddressOfExpr addrExpr | addrExpr.getOperand() = call) and + // Ignore results in the xmpsdk directory. + not call.getLocation().getFile().getRelativePath().matches("xmpsdk/%") +select call, "Unsafe use of operator[]. Use the at() method instead." diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 0000000000..ea1ab97ceb --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,5 @@ +name: "Exiv2 CodeQL config" + +queries: + - uses: ./.github/codeql-queries/exiv2-code-scanning.qls + - uses: ./.github/codeql-queries/exiv2-cpp-queries diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..5ace4600a1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml new file mode 100644 index 0000000000..e18449dd79 --- /dev/null +++ b/.github/workflows/cifuzz.yml @@ -0,0 +1,33 @@ +# OSS-Fuzz continuous integration: +# https://google.github.io/oss-fuzz/getting-started/continuous-integration/ + +name: CIFuzz +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +on: + pull_request: + paths-ignore: + - "*.md" +jobs: + Fuzzing: + runs-on: ubuntu-latest + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: 'exiv2' + dry-run: false + - name: Run Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: 'exiv2' + fuzz-seconds: 600 + dry-run: false + - name: Upload Crash + uses: actions/upload-artifact@v7 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..3ea542e53c --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,75 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + push: + branches: [0.28.x, main] + paths-ignore: + - "*.md" + pull_request: + # The branches below must be a subset of the branches above + branches: [0.28.x, main] + paths-ignore: + - "*.md" + workflow_dispatch: + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'cpp', 'javascript', 'actions' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo eatmydata apt-get -y install libexpat1-dev zlib1g-dev libbrotli-dev libinih-dev + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + config-file: .github/codeql/codeql-config.yml + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v4 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/nightly_Linux_distributions.yml b/.github/workflows/nightly_Linux_distributions.yml new file mode 100644 index 0000000000..ec6b1026fc --- /dev/null +++ b/.github/workflows/nightly_Linux_distributions.yml @@ -0,0 +1,45 @@ +on: + schedule: + - cron: 0 4 * * * + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +name: Nightly - Linux distributions + +jobs: + distros: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + container_image: ["fedora", "debian", "archlinux", "ubuntu", "tgagor/centos", "alpine"] + compiler: + - { CC: gcc, CXX: g++ } + - { CC: clang, CXX: clang++ } + build_type: [Release, Debug] + shared_libraries: [ON, OFF] + container: + image: ${{ matrix.container_image }} + env: + CC: ${{ matrix.compiler.CC }} + CXX: ${{ matrix.compiler.CXX }} + CMAKE_FLAGS: -DEXIV2_TEAM_EXTRA_WARNINGS=OFF -DEXIV2_ENABLE_WEBREADY=ON -DEXIV2_ENABLE_CURL=ON -DEXIV2_BUILD_UNIT_TESTS=OFF -DEXIV2_TEAM_WARNINGS_AS_ERRORS=OFF -DCMAKE_INSTALL_PREFIX=install + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Install dependencies + run: ./ci/install_dependencies.sh + - name: Build and install + run: | + cmake -GNinja $CMAKE_FLAGS -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DBUILD_SHARED_LIBS=${{ matrix.shared_libraries }} -S . -B build + cmake --build build --parallel + cmake --install build diff --git a/.github/workflows/on_PR_linux_fuzz.yml b/.github/workflows/on_PR_linux_fuzz.yml new file mode 100644 index 0000000000..be25eee04f --- /dev/null +++ b/.github/workflows/on_PR_linux_fuzz.yml @@ -0,0 +1,56 @@ +# Builds and runs the fuzz target for a short amount of time. This is +# mainly to protect the fuzz target from bitrot, but hopefully will +# also help to quickly catch some bugs before the PR is merged. + +name: On PRs - Linux-Ubuntu Quick Fuzz + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + pull_request: + paths-ignore: + - "*.md" + workflow_dispatch: + +jobs: + Linux: + name: 'Ubuntu 24.04 - clang/libFuzzer' + runs-on: ubuntu-24.04 + + strategy: + fail-fast: false + matrix: + fuzz_target: [fuzz-read-write, fuzz-read-print-write] + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Install dependencies + run: | + sudo ./ci/install_dependencies.sh + + - name: Build + env: + CC: clang + CXX: clang++ + run: | + cmake --preset linux-sanitizers -S . -B build -DCMAKE_BUILD_TYPE=Release -DEXIV2_BUILD_FUZZ_TESTS=ON -DEXIV2_BUILD_UNIT_TESTS=OFF + cmake --build build --parallel + + - name: Fuzz + run: | + cd build + mkdir corpus + LSAN_OPTIONS=suppressions=../fuzz/knownleaks.txt ./bin/${{matrix.fuzz_target}} corpus ../test/data/ -dict=../fuzz/exiv2.dict -jobs=$(nproc) -workers=$(nproc) -max_len=4096 -max_total_time=120 + + - uses: actions/upload-artifact@v7 + with: + name: exiv2-${{ matrix.fuzz_target }} + path: | + ./build/crash-* + ./build/leak-* + ./build/timeout-* + if-no-files-found: ignore diff --git a/.github/workflows/on_PR_linux_matrix.yml b/.github/workflows/on_PR_linux_matrix.yml new file mode 100644 index 0000000000..74ab374a00 --- /dev/null +++ b/.github/workflows/on_PR_linux_matrix.yml @@ -0,0 +1,92 @@ +name: On PRs - Linux-Ubuntu Matrix + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + pull_request: + paths-ignore: + - "*.md" + +jobs: + Linux: + name: 'Ubuntu 22.04 - GCC, BuildType:${{matrix.build_type}}, SHARED:${{matrix.shared_libraries}}' + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + build_type: [Release, Debug] + shared_libraries: [ON, OFF] + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo apt install -y tree ninja-build + python3 -m pip install conan==1.* lxml + + - name: Conan common config + run: | + conan profile new --detect default + conan profile update settings.build_type=${{matrix.build_type}} default + conan profile update settings.compiler.libcxx=libstdc++11 default + + - name: Run Conan + run: | + mkdir build-base_linux && cd build-base_linux + conan profile list + conan profile show default + conan install .. -o webready=True --build missing + + - name: Build + run: | + cmake --preset base_linux -S . -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -DBUILD_SHARED_LIBS=${{matrix.shared_libraries}} + cmake --build build-base_linux --parallel + + - name: Install + run: | + cd build-base_linux + cmake --install . + + - name: Test + run: | + cd build-base_linux + ctest --output-on-failure + Alpine: + name: 'Alpine Edge - GCC, BuildType:${{matrix.build_type}}, SHARED:${{matrix.shared_libraries}} ARCH:${{matrix.arch}}' + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + arch: ['loongarch64', 'riscv64', 's390x'] + build_type: [Release, Debug] + shared_libraries: [ON, OFF] + + defaults: + run: + shell: alpine.sh {0} + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: jirutka/setup-alpine@v1 + with: + arch: ${{matrix.arch}} + branch: edge + packages: > + build-base cmake samurai brotli-dev curl-dev expat-dev inih-inireader-dev gtest-dev python3-dev zlib-dev + - name: Build + run: | + cmake --preset base_linux -S . -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -DBUILD_SHARED_LIBS=${{matrix.shared_libraries}} -DCONAN_AUTO_INSTALL=OFF + cmake --build build-base_linux --parallel + - name: Test + run: | + cd build-base_linux + ctest --output-on-failure diff --git a/.github/workflows/on_PR_linux_special_builds.yml b/.github/workflows/on_PR_linux_special_builds.yml new file mode 100644 index 0000000000..006e5a7fea --- /dev/null +++ b/.github/workflows/on_PR_linux_special_builds.yml @@ -0,0 +1,198 @@ +name: On PRs - Linux Special Builds + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + workflow_dispatch: + pull_request: + paths-ignore: + - "*.md" + +jobs: + special_debugRelease: + name: 'Ubuntu 22.04 - GCC - Debug+Coverage' + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 2 + persist-credentials: false + # Trying to deal with warning: -> Issue detecting commit SHA. Please run actions/checkout with fetch-depth > 1 or set to 0 + + - name: Install dependencies + run: | + sudo eatmydata apt-get -y install libxml2-dev libxslt-dev python3-dev ninja-build gcovr + python3 -m pip install conan==1.* lxml + + - name: Conan common config + run: | + conan profile new --detect default + conan profile update settings.compiler.libcxx=libstdc++11 default + + - name: Run Conan + run: | + mkdir build && cd build + conan profile list + conan profile show default + conan install .. -o webready=True --build missing + + - name: Build + run: | + cmake --preset linux-coverage -S . -B build + cmake --build build --parallel + + - name: Tests + Upload coverage + run: | + cd build + ctest --output-on-failure + # this needs to match th ecommand in on_push_ExtraJobsForMain.yml! + gcovr --root .. --object-dir . --exclude-unreachable-branches --exclude-throw-branches --xml -o coverage.xml + curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --import + curl -Os https://uploader.codecov.io/latest/linux/codecov + curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM + curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig + gpg --verify codecov.SHA256SUM.sig codecov.SHA256SUM + shasum -a 256 -c codecov.SHA256SUM + chmod +x codecov + ls -lh + ./codecov -f coverage.xml + + special_releaseValgrind: + name: 'Ubuntu 22.04 - GCC - Release+Valgrind' + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo eatmydata apt-get -y update + sudo eatmydata apt-get -y install valgrind ninja-build + python3 -m pip install conan==1.* lxml + + - name: Conan common config + run: | + conan profile new --detect default + conan profile update settings.compiler.libcxx=libstdc++11 default + + - name: Run Conan + run: | + mkdir build && cd build + conan profile list + conan profile show default + conan install .. -o webready=True --build missing + + - name: Build + run: | + cmake --preset linux-release -S . -B build + cmake --build build --parallel + + - name: Tests with valgrind + run: | + cd build/bin + valgrind ./unit_tests + + + special_releaseSanitizers: + name: 'Ubuntu 22.04 - GCC - Release+Sanitizers' + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo apt install -y ninja-build + python3 -m pip install conan==1.* lxml + + - name: Conan common config + run: | + conan profile new --detect default + conan profile update settings.compiler.libcxx=libstdc++11 default + + - name: Run Conan + run: | + mkdir build && cd build + conan profile list + conan profile show default + conan install .. -o webready=True --build missing + + - name: Build + run: | + cmake --preset linux-sanitizers -S . -B build + cmake --build build --parallel + + - name: Tests + run: | + cd build + ctest --output-on-failure + + special_noFilesystemAccess: + name: 'Ubuntu 22.04 - GCC - No filesystem access build' + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo apt install -y ninja-build + python3 -m pip install conan==1.* lxml + + - name: Conan common config + run: | + conan profile new --detect default + conan profile update settings.compiler.libcxx=libstdc++11 default + + - name: Build + run: | + cmake --preset linux-release -S . -B build -DEXIV2_ENABLE_FILESYSTEM_ACCESS=OFF -DEXIV2_BUILD_SAMPLES=OFF -DEXIV2_BUILD_UNIT_TESTS=OFF -DEXIV2_BUILD_EXIV2_COMMAND=OFF + cmake --build build --parallel + + special_allEnabled: + name: 'Ubuntu 22.04 - GCC - All Options Enabled + Documentation' + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo eatmydata apt-get -y update + sudo eatmydata apt-get -y install valgrind doxygen graphviz gettext ninja-build + python3 -m pip install conan==1.* lxml + + - name: Conan common config + run: | + conan profile new --detect default + conan profile update settings.compiler.libcxx=libstdc++11 default + + - name: Run Conan + run: | + mkdir build && cd build + conan profile list + conan profile show default + conan install .. -o webready=True --build missing + + - name: Build + env: + CXXFLAGS: -DEXIV2_DEBUG_MESSAGES + run: | + cmake --preset linux-release -S . -B build -DEXIV2_BUILD_DOC=ON + cmake --build build --parallel + + - name: Generate documentation + run: | + make doc + diff --git a/.github/workflows/on_PR_linux_staticAnalysis.yml b/.github/workflows/on_PR_linux_staticAnalysis.yml new file mode 100644 index 0000000000..2c159b9cf4 --- /dev/null +++ b/.github/workflows/on_PR_linux_staticAnalysis.yml @@ -0,0 +1,66 @@ +name: On PRs - Linux - Static Analysis + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + workflow_dispatch: + pull_request: + paths-ignore: + - "*.md" + +jobs: + special_pvsStudio: + name: 'Ubuntu 22.04 - GCC - Static Analyzer: PVS-Studio' + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 2 + persist-credentials: false + # Trying to deal with warning: -> Issue detecting commit SHA. Please run actions/checkout with fetch-depth > 1 or set to 0 + + - name: Install dependencies + run: | + python3 -m pip install conan==1.* + sudo add-apt-repository ppa:ubuntu-lxc/daily -y + wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt |sudo apt-key add - + sudo wget -O /etc/apt/sources.list.d/viva64.list https://files.pvs-studio.com/etc/viva64.list + sudo apt update -qq + sudo apt install -qq pvs-studio + + - name: Conan common config + run: | + conan profile new --detect default + conan profile update settings.compiler.libcxx=libstdc++11 default + + - name: Run Conan + run: | + mkdir build && cd build + conan profile list + conan profile show default + conan install .. -o webready=True --build missing + + - name: Configure + run: | + cmake --preset linux-debug -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + - name: Static Analysis + env: + PVS_USERNAME: ${{ secrets.PVS_USERNAME }} + PVS_KEY: ${{ secrets.PVS_KEY }} + run: | + cd build + pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS_license.lic + pvs-studio-analyzer analyze -l PVS_license.lic -o pvsStudio.log -j4 --disableLicenseExpirationCheck + plog-converter -a GA:1,2 -d V1042 -t fullhtml pvsStudio.log -o pvsReportHtml + + - uses: actions/upload-artifact@v7 + with: + name: static_analysis + path: build/pvsReportHtml + retention-days: 7 + + diff --git a/.github/workflows/on_PR_mac_matrix.yml b/.github/workflows/on_PR_mac_matrix.yml new file mode 100644 index 0000000000..5bb466a52e --- /dev/null +++ b/.github/workflows/on_PR_mac_matrix.yml @@ -0,0 +1,44 @@ +name: On PRs - Mac Matrix + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + pull_request: + paths-ignore: + - "*.md" + +jobs: + MacOS: + name: 'macOS - XCode - ${{matrix.build_type}} - SHARED:${{matrix.shared_libraries}}' + runs-on: macos-latest + + strategy: + fail-fast: false + matrix: + build_type: [Release, Debug] + shared_libraries: [ON, OFF] + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + brew install inih googletest + + - name: Build + run: | + cmake --preset base_mac -S . -B build -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -DBUILD_SHARED_LIBS=${{matrix.shared_libraries}} + cmake --build build --parallel + + - name: Install + run: | + cd build + cmake --install . + + - name: Test + run: | + ctest --test-dir build --output-on-failure diff --git a/.github/workflows/on_PR_mac_special_builds.yml b/.github/workflows/on_PR_mac_special_builds.yml new file mode 100644 index 0000000000..38b187c798 --- /dev/null +++ b/.github/workflows/on_PR_mac_special_builds.yml @@ -0,0 +1,34 @@ +name: On PRs - Mac Special Builds + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + workflow_dispatch: + pull_request: + paths-ignore: + - "*.md" + +jobs: + MacOS_releaseSanitizers: + name: 'macOS - XCode - Release+Sanitizers' + runs-on: macos-latest + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + brew install inih googletest + + - name: Build + run: | + cmake --preset base_mac -S . -B build -DEXIV2_TEAM_USE_SANITIZERS=ON + cmake --build build --parallel + + - name: Test + run: | + ASAN_OPTIONS=detect_container_overflow=0 ctest --test-dir build --output-on-failure diff --git a/.github/workflows/on_PR_meson.yaml b/.github/workflows/on_PR_meson.yaml new file mode 100644 index 0000000000..b59a06c657 --- /dev/null +++ b/.github/workflows/on_PR_meson.yaml @@ -0,0 +1,249 @@ +name: On PRs - meson + +on: pull_request + +concurrency: + group: ${{github.workflow}}-${{github.head_ref}} + cancel-in-progress: true + +jobs: + Ubuntu: + runs-on: ubuntu-22.04 + name: Linux-GCC${{matrix.cxx}}-deps=${{matrix.deps}} + strategy: + matrix: + cxx: ['9', '13'] + deps: ['enabled', 'disabled'] + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: egor-tensin/setup-gcc@v2 + with: + version: ${{matrix.cxx}} + - name: Install meson + run: python3 -m pip install lxml meson ninja + - name: Install dependencies + run: sudo apt install -y cmake libcurl4-openssl-dev libbrotli-dev libexpat-dev libfmt-dev libinih-dev libz-dev gettext + - name: Compile and Test + run: | + meson setup "${{github.workspace}}/build" -Dauto_features=${{matrix.deps}} -Dwarning_level=3 + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + Ubuntu-clang: + runs-on: ubuntu-22.04 + name: Linux-Clang${{matrix.cxx}}-deps=${{matrix.deps}} + strategy: + matrix: + cxx: ['11', '21'] + deps: ['enabled', 'disabled'] + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: egor-tensin/setup-clang@v2 + with: + version: ${{matrix.cxx}} + - name: Install meson + run: | + python3 -m pip install lxml meson ninja + - name: Install dependencies + run: | + sudo apt install -y libcurl4-openssl-dev libbrotli-dev libz-dev gettext + sudo apt install -y libc++abi-${{matrix.cxx}}-dev libc++-${{matrix.cxx}}-dev + - name: Compile and Test + env: + CXXFLAGS: -stdlib=libc++ + run: | + meson setup "${{github.workspace}}/build" -Dauto_features=${{matrix.deps}} -Dwarning_level=3 + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + VisualStudio: + runs-on: ${{matrix.runner}} + name: MSVC-${{matrix.deps}}-${{matrix.platform}} + strategy: + matrix: + deps: ['forcefallback', 'default'] + platform: ['arm64', 'x64', 'x86'] + include: + - platform: arm64 + runner: windows-11-arm + - platform: x64 + runner: windows-latest + - platform: x86 + runner: windows-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install packages + run: | + python -m pip install lxml meson ninja + + - uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{matrix.platform}} + + - name: Compile and Test + run: | + meson setup "${{github.workspace}}/build" --wrap-mode=${{matrix.deps}} -Dwarning_level=3 -Dcpp_std=c++latest -Ddefault_library=static + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + VisualStudio-clang-cl: + runs-on: ${{matrix.runner}} + name: clang-cl-${{matrix.deps}}-${{matrix.platform}} + strategy: + matrix: + deps: ['forcefallback', 'default'] + platform: ['arm64', 'x64'] + include: + - platform: arm64 + runner: windows-11-arm + - platform: x64 + runner: windows-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install packages + run: | + python -m pip install lxml meson ninja + + - uses: ilammy/msvc-dev-cmd@v1 + + - name: Compile and Test + env: + CC: clang-cl + CXX: clang-cl + run: | + meson setup "${{github.workspace}}/build" --wrap-mode=${{matrix.deps}} -Dwarning_level=3 -Dcpp_std=c++latest -Db_sanitize=address -Ddefault_library=static + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + MSYS2: + runs-on: ${{matrix.runner}} + name: MSYS2-${{matrix.platform}}-deps=${{matrix.deps}} + strategy: + matrix: + deps: ['enabled', 'disabled'] + platform: ['UCRT64', 'CLANGARM64'] + include: + - platform: UCRT64 + runner: windows-latest + - platform: CLANGARM64 + runner: windows-11-arm + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - uses: msys2/setup-msys2@v2 + with: + msystem: ${{matrix.platform}} + pacboy: >- + cc:p + cmake:p + curl:p + gtest:p + libinih:p + meson:p + ninja:p + pkgconf:p + python-lxml:p + + - name: Compile and Test + run: | + meson setup "${{github.workspace}}/build" -Dauto_features=${{matrix.deps}} -Dwarning_level=3 -Ddefault_library=static + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + MSYS: + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: msys2/setup-msys2@v2 + with: + msystem: 'MSYS' + install: >- + cmake + gcc + meson + ninja + pkgconf + - name: Compile and Test + run: | + meson setup build -Dwarning_level=3 -Dcpp_std=gnu++20 + meson compile -C build --verbose + meson test -C build --verbose + MacOS: + runs-on: macos-latest + name: macOS-deps=${{matrix.deps}} + strategy: + matrix: + deps: ['enabled', 'disabled'] + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install packages + run: | + brew install inih meson + python -m pip install lxml + + - name: Compile and Test + run: | + meson setup "${{github.workspace}}/build" -Dauto_features=${{matrix.deps}} -Dwarning_level=3 -Dnls=disabled -Db_lundef=false -Db_sanitize=address,undefined + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + FreeBSD: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: vmactions/freebsd-vm@v1 + with: + prepare: | + pkg install -y cmake curl ninja meson gettext python pkgconf googletest expat inih brotli + run: | + meson setup "${{github.workspace}}/build" -Dwarning_level=3 + meson compile -C "${{github.workspace}}/build" --verbose + meson test -C "${{github.workspace}}/build" --verbose + OmniOS: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: vmactions/omnios-vm@v1 + with: + prepare: | + pkg install brotli cmake curl expat gcc14 meson-313 ninja pkg-config python-313 + run: | + /usr/lib/python3.13/bin/meson setup "${{github.workspace}}/build" -Dwarning_level=3 -DunitTests=enabled -Dinih=enabled + /usr/lib/python3.13/bin/meson compile -C "${{github.workspace}}/build" --verbose + /usr/lib/python3.13/bin/meson test -C "${{github.workspace}}/build" --verbose + Emscripten: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Install packages + run: | + sudo apt install -y meson + - name: Emscripten + uses: mymindstorm/setup-emsdk@v14 + - name: Compile + run: | + meson setup "${{github.workspace}}/build" --cross-file="${{github.workspace}}/em.txt" --wrap-mode=forcefallback -Ddefault_library=static -Dwarning_level=3 -DunitTests=disabled -Dcurl=disabled + meson compile -C "${{github.workspace}}/build" --verbose diff --git a/.github/workflows/on_PR_windows_matrix.yml b/.github/workflows/on_PR_windows_matrix.yml new file mode 100644 index 0000000000..18c833df42 --- /dev/null +++ b/.github/workflows/on_PR_windows_matrix.yml @@ -0,0 +1,193 @@ +name: On PRs - Windows Matrix + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + pull_request: + paths-ignore: + - "*.md" + workflow_dispatch: + push: + branches: [0.28.x, main] + tags: + - '!*' + paths-ignore: + - "*.md" + +jobs: + windows: + name: 'Win10 Arch: ${{matrix.platform}} BuildType:${{matrix.build_type}} - SHARED:${{matrix.shared_libraries}}' + runs-on: windows-2022 + + strategy: + fail-fast: false + matrix: + build_type: [Release, Debug] + shared_libraries: [ON, OFF] + platform: [ x64, x86 ] + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Visual Studio shell + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{matrix.platform}} + + - name: Restore Conan cache + uses: actions/cache@v5 + with: + path: ${{github.workspace}}/conanCache + key: ${{runner.os}}-${{matrix.platform}}-${{matrix.build_type}}-Shared${{matrix.shared_libraries}}-${{ hashFiles('conanfile.py') }} + lookup-only: true + + - name: Install Conan & Common config + run: | + python -m pip install conan==1.* lxml + conan config install https://github.com/conan-io/conanclientcert.git + conan profile new --detect default + conan profile update settings.build_type=${{matrix.build_type}} default + conan profile update settings.compiler="Visual Studio" default + conan profile update settings.compiler.version=17 default + conan config set storage.path=$Env:GITHUB_WORKSPACE/conanCache + + - name: Conan Arch conditional config + if: ${{matrix.platform == 'x86'}} + run: | + conan profile update settings.arch=x86 default + conan profile update settings.arch_build=x86 default + + - name: Run Conan + run: | + md build + cd build + conan profile list + conan install .. --build missing + + - name: Build + run: | + cmake --preset base_windows -S . -B build -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -DBUILD_SHARED_LIBS=${{matrix.shared_libraries}} + cmake --build build --parallel + + - name: Install + run: | + cmake --install build + + - name: Test + run: | + ctest --test-dir build --output-on-failure + + msys2: + runs-on: windows-latest + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + build_type: [Release, Debug] + shared_libraries: [ON, OFF] + sys: [UCRT64] + name: MSYS2 ${{matrix.sys}} - BuildType:${{matrix.build_type}} - SHARED:${{matrix.shared_libraries}} + defaults: + run: + shell: msys2 {0} + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up MSYS2 + uses: msys2/setup-msys2@v2 + with: + path-type: strict + msystem: ${{matrix.sys}} + update: true + pacboy: >- + cc:p + gcc-libs:p + libwinpthread:p + cmake:p + ninja:p + python:p + gtest:p + brotli:p + curl:p + expat:p + libiconv:p + libinih:p + zlib:p + + - name: Build + run: | + cmake --preset base_windows \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DBUILD_SHARED_LIBS=${{matrix.shared_libraries}} \ + -DCONAN_AUTO_INSTALL=OFF \ + -DEXIV2_TEAM_WARNINGS_AS_ERRORS=OFF \ + -DPython3_EXECUTABLE=${MINGW_PREFIX}/bin/python.exe \ + -S . -B build && \ + cmake --build build --parallel + + - name: Test + run: | + ctest --test-dir build --output-on-failure + + cygwin: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + build_type: [Release] + shared_libraries: [ON] + platform: [x86_64] + name: Cygwin ${{matrix.platform}} - BuildType:${{matrix.build_type}} - SHARED:${{matrix.shared_libraries}} + env: + SHELLOPTS: igncr + defaults: + run: + shell: C:\cygwin\bin\bash.exe -eo pipefail '{0}' + steps: + # Make sure we don't check out scripts using Windows CRLF line endings + - run: git config --global core.autocrlf input + shell: pwsh + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Cygwin + uses: cygwin/cygwin-install-action@v6 + with: + platform: ${{matrix.platform}} + packages: >- + gcc-g++ + cmake + ninja + pkg-config + python3 + libbrotli-devel + libcurl-devel + libexpat-devel + libiconv-devel + libinih-devel + zlib-devel + + - name: Build + run: | + cmake --preset base_windows \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DBUILD_SHARED_LIBS=${{matrix.shared_libraries}} \ + -DCONAN_AUTO_INSTALL=OFF \ + -DEXIV2_BUILD_SAMPLES=OFF \ + -DEXIV2_BUILD_UNIT_TESTS=OFF \ + -DEXIV2_TEAM_WARNINGS_AS_ERRORS=OFF \ + -DPython3_EXECUTABLE=/usr/bin/python3 \ + -S . -B build && \ + cmake --build build --parallel + + - name: Test + run: | + ctest --test-dir build --output-on-failure diff --git a/.github/workflows/on_push_BasicWinLinMac.yml b/.github/workflows/on_push_BasicWinLinMac.yml new file mode 100644 index 0000000000..211093d848 --- /dev/null +++ b/.github/workflows/on_push_BasicWinLinMac.yml @@ -0,0 +1,112 @@ +# Basic CI for all platforms on push +# Note that we want to run this as fast as possible and just for the more common configurations. On +# PRs, we will test things more intensively :) +# - Only running UnitTests and not regression tests + +on: + push: + paths-ignore: + - "*.md" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +name: On PUSH - Basic CI for main platforms + +jobs: + windows: + name: 'Windows 10 - MSVC - Arch:X64 BuildType:Release - SHARED' + runs-on: windows-2022 + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Visual Studio shell + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Restore Conan cache + uses: actions/cache@v5 + with: + path: ${{github.workspace}}/conanCache + key: ${{runner.os}}-push-win-${{ hashFiles('conanfile.py') }} + + - name: Install Conan & Common config + run: | + python -m pip install conan==1.* lxml + conan profile new --detect default + conan profile show default + conan profile update settings.compiler="Visual Studio" default + conan profile update settings.compiler.version=17 default + conan config set storage.path=$Env:GITHUB_WORKSPACE/conanCache + + - name: Build + run: | + cmake --preset win-release -S . -B build + cmake --build build --parallel + + - name: Test + run: | + ctest --test-dir build --output-on-failure + + Linux: + name: 'Ubuntu 22.04 - GCC - Arch:X64 BuildType:Release - SHARED' + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo eatmydata apt-get -y install ninja-build + python3 -m pip install conan==1.* lxml + + - name: Conan + run: | + mkdir build && cd build + conan profile new --detect default + conan profile update settings.compiler.libcxx=libstdc++11 default + conan profile show default + conan install .. -o webready=True --build missing + + - name: Build + run: | + cmake --preset linux-release -S . -B build + cmake --build build --parallel + + - name: Test + run: | + ctest --test-dir build --output-on-failure + + MacOS: + name: 'macOS - XCode - Arch:${{ matrix.runner.arch }} BuildType:Release - SHARED' + runs-on: ${{ matrix.runner.os }} + strategy: + matrix: + runner: + - { os: macos-15-intel, arch: X64 } + - { os: macos-latest, arch: ARM64 } + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + brew install inih googletest + + - name: Build + run: | + cmake --preset base_mac -S . -B build + cmake --build build --parallel + + - name: Test + run: | + ctest --test-dir build --output-on-failure diff --git a/.github/workflows/on_push_ExtraJobsForMain.yml b/.github/workflows/on_push_ExtraJobsForMain.yml new file mode 100644 index 0000000000..5e4f95eac7 --- /dev/null +++ b/.github/workflows/on_push_ExtraJobsForMain.yml @@ -0,0 +1,62 @@ +name: On PUSH - Linux Special Builds for main branch + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +on: + push: + branches: + - main + tags: + - '!*' + paths-ignore: + - "*.md" + workflow_dispatch: + +jobs: + special_debugRelease: + name: 'Ubuntu 22.04 - GCC - Debug+Coverage' + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo eatmydata apt-get -y install ninja-build gcovr + python3 -m pip install conan==1.* lxml + + - name: Conan common config + run: | + conan profile new --detect default + conan profile update settings.compiler.libcxx=libstdc++11 default + + - name: Run Conan + run: | + mkdir build && cd build + conan profile list + conan profile show default + conan install .. -o webready=True --build missing + + - name: Build + run: | + cmake --preset linux-coverage -S . -B build + cmake --build build + + - name: Tests + Upload coverage + run: | + cd build + ctest --output-on-failure + # this needs to match th ecommand in on_PR_linux_secial_builds.yml! + gcovr --root .. --object-dir . --exclude-unreachable-branches --exclude-throw-branches --xml -o coverage.xml . + curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --import + curl -Os https://uploader.codecov.io/latest/linux/codecov + curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM + curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig + gpg --verify codecov.SHA256SUM.sig codecov.SHA256SUM + shasum -a 256 -c codecov.SHA256SUM + chmod +x codecov + ./codecov -f build/coverage.xml diff --git a/.github/workflows/on_push_clang_format.yml b/.github/workflows/on_push_clang_format.yml new file mode 100644 index 0000000000..9826571d18 --- /dev/null +++ b/.github/workflows/on_push_clang_format.yml @@ -0,0 +1,18 @@ +name: Clang Format Checker +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +on: [push, pull_request] +jobs: + clang-format-checking: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: DoozyX/clang-format-lint-action@v0.20 + with: + source: '.' + exclude: './xmpsdk ./contrib' + extensions: 'c,h,cpp,hpp' + style: file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..dadad65ad5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,191 @@ +name: Release +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +on: + push: + tags: + - v[0-9]+.[0-9]+.[0-9]+ + schedule: + - cron: '30 4 * * *' + workflow_dispatch: + +jobs: + Linux: + name: 'Build Linux Release' + runs-on: ${{ matrix.runner.os }} + strategy: + matrix: + runner: + - { os: ubuntu-22.04, arch: x64 } + - { os: ubuntu-22.04-arm, arch: arm64 } + permissions: + id-token: write # For sigstore + attestations: write # For artifact attestation + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo apt install -y gettext doxygen graphviz ninja-build + python -m pip install conan==1.* + + - name: Conan common config + run: | + conan profile new --detect default + conan profile update settings.build_type=Release default + conan profile update settings.compiler.libcxx=libstdc++11 default + + - name: Run Conan + run: | + mkdir build && cd build + conan profile list + conan profile show default + conan install .. -o webready=False --build missing + + - name: Build packaged release + run: | + cmake --preset linux-all -S . -B build -DEXIV2_TEAM_PACKAGING=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -DEXIV2_ENABLE_WEBREADY=OFF -DEXIV2_ENABLE_CURL=OFF -DEXIV2_BUILD_SAMPLES=OFF + cmake --build build -t doc + cmake --build build -t package + + - name: Attest build provenance + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: ./build/exiv2-*.tar.gz + + - uses: actions/upload-artifact@v7 + with: + name: exiv2-${{ matrix.runner.arch }}-${{ matrix.runner.os }} + path: ./build/exiv2-*.tar.gz + if-no-files-found: error + retention-days: 1 + + macOS: + name: 'Build macOS Release' + runs-on: ${{ matrix.runner.os }} + strategy: + matrix: + runner: + - { os: macos-15-intel, arch: x64 } + - { os: macos-14, arch: arm64 } + permissions: + id-token: write # For sigstore + attestations: write # For artifact attestation + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install dependencies + run: | + brew install inih tree doxygen graphviz + + - name: Build packaged release + run: | + cmake -GNinja -S . -B build \ + -DEXIV2_TEAM_PACKAGING=ON \ + -DBUILD_SHARED_LIBS=ON \ + -DEXIV2_ENABLE_WEBREADY=OFF \ + -DEXIV2_ENABLE_NLS=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DEXIV2_TEAM_WARNINGS_AS_ERRORS=ON \ + -DEXIV2_BUILD_DOC=ON \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON + cmake --build build -t doc + cmake --build build -t package + + - name: Attest build provenance + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: ./build/exiv2-*.tar.gz + + - uses: actions/upload-artifact@v7 + with: + name: exiv2-${{ matrix.runner.arch }}-${{ matrix.runner.os }} + path: ./build/exiv2-*.tar.gz + if-no-files-found: error + retention-days: 1 + + Windows: + name: 'Build Windows Release' + runs-on: ${{ matrix.runner.os }} + strategy: + matrix: + runner: + - { os: windows-2022, arch: x64 } + permissions: + id-token: write # For sigstore + attestations: write # For artifact attestation + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Visual Studio shell + uses: ilammy/msvc-dev-cmd@v1 + + - name: Install doxygen + run: | + choco install doxygen.install + choco install graphviz + + - name: Restore conan cache + uses: actions/cache@v5 + with: + path: ${{github.workspace}}/conanCache + key: ${{runner.os}}-release-win-${{ hashFiles('conanfile.py') }} + lookup-only: true + + - name: Install Conan & Common config + run: | + python -m pip install conan==1.* + conan profile new --detect default + conan profile show default + conan profile update settings.build_type=Release default + conan profile update settings.compiler="Visual Studio" default + conan profile update settings.compiler.version=17 default + conan config set storage.path=$Env:GITHUB_WORKSPACE/conanCache + + - name: Run Conan + run: | + md build + cd build + conan install .. --build missing + + - name: Build packaged release + run: | + cmake --preset win-release -S . -B build -DEXIV2_TEAM_PACKAGING=ON -DEXIV2_BUILD_DOC=ON -DEXIV2_ENABLE_WEBREADY=OFF -DEXIV2_ENABLE_CURL=OFF -DEXIV2_BUILD_SAMPLES=OFF + cmake --build build --parallel -t doc + cmake --build build --parallel -t package + + - name: Attest build provenance + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: ./build/exiv2-*.zip + + - uses: actions/upload-artifact@v7 + with: + name: exiv2-${{ matrix.runner.arch }}-${{ matrix.runner.os }} + path: ./build/exiv2-*.zip + if-no-files-found: error + retention-days: 1 + + publish: + needs: [Linux, macOS, Windows] + runs-on: ubuntu-22.04 + permissions: + contents: write + if: github.event_name == 'push' # only publish when a new version tag is pushed + steps: + - uses: actions/download-artifact@v8 + - name: List downloaded files + run: tree -L 3 + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPO: ${{ github.repository }} + RELEASE_NAME: ${{ github.ref_name }} + run: gh release create "$RELEASE_NAME" ./exiv2-*/exiv2-* --repo "$GITHUB_REPO" --generate-notes diff --git a/.gitignore b/.gitignore index d3090138aa..dd45d6861a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,9 @@ src/doxygen.hpp test/tmp/* doc/html contrib/vms/.vagrant -/.vscode \ No newline at end of file +/.vscode +.vs/ +CMakeUserPresets.json + +*cppcheck* +subprojects/packagecache diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 6881cb651e..0000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,99 +0,0 @@ -# all builds use the same ccache folder in the project root that is cached -variables: - CCACHE_BASEDIR: '$CI_PROJECT_DIR' - CCACHE_DIR: '$CI_PROJECT_DIR/ccache' - -# default config for all distros: -# - install dependencies via script -# - create ccache dir & setup caching of it (for each job separately) -.build_config: &default_config - before_script: - - ci/install_dependencies.sh - - mkdir -p ccache - cache: - key: "$CI_JOB_NAME" - paths: - - ccache/ - -# default build job: -# - run build script -# - only create artifacts of the build directory when something fails -# (for cmake logs) -.build_template: &distro_build - script: - - python3 ci/test_build.py - artifacts: - when: on_failure - paths: - - build/ - -stages: - - test - - deploy - -# Fedora: -# image: fedora:latest -# <<: *default_config -# <<: *distro_build - -Fedora_MinGW: - image: fedora:latest - before_script: - - dnf -y upgrade - - dnf -y install mingw64-gcc-c++ mingw64-filesystem mingw64-expat mingw64-zlib cmake make - script: - - python3 ci/test_build.py --without-tests --cmake-executable "mingw64-cmake" --cmake-options "-DEXIV2_TEAM_EXTRA_WARNINGS=ON -DEXIV2_ENABLE_VIDEO=ON -DEXIV2_ENABLE_WEBREADY=ON -DEXIV2_ENABLE_WIN_UNICODE=ON " --compilers --shared-libs OFF - -Debian: - image: debian:9 - <<: *default_config - <<: *distro_build - -# Archlinux: -# image: archlinux/base -# <<: *default_config -# <<: *distro_build - -Ubuntu: - image: ubuntu:18.04 - <<: *default_config - <<: *distro_build - -# CentOS: -# image: centos:7 -# <<: *default_config -# <<: *distro_build - -# OpenSUSE: -# image: opensuse/tumbleweed -# <<: *default_config -# <<: *distro_build - -Install: - image: fedora:latest - stage: deploy - <<: *default_config - script: - - mkdir build && cd build - - cmake -DCMAKE_BUILD_TYPE=Release -DEXIV2_ENABLE_VIDEO=ON -DEXIV2_ENABLE_WEBREADY=ON -DEXIV2_BUILD_UNIT_TESTS=ON -DCMAKE_INSTALL_PREFIX=/usr/ -DBUILD_WITH_CCACHE=ON .. - - make -j $(nproc) - - make install - - make clean - - EXIV2_BINDIR=/usr/bin/ make tests - -pages: - image: fedora:latest - stage: deploy - <<: *default_config - script: - - dnf -y install doxygen graphviz - - mkdir build && cd build - - cmake -DCMAKE_BUILD_TYPE=Release -DEXIV2_ENABLE_VIDEO=ON -DEXIV2_ENABLE_WEBREADY=ON -DEXIV2_BUILD_DOC=ON .. - - make doc - - cd .. - - mv build/doc/html/ public/ - artifacts: - paths: - - public - only: - - master diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 0000000000..a5fa631b94 --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,23 @@ +pull_request_rules: + - name: forward patches to main branch + conditions: + - base=0.28.x + - label=forward-to-main + actions: + backport: + branches: + - main + assignees: + - "{{ author }}" + + - name: delete head branch after merge + conditions: + - merged + actions: + delete_head_branch: {} + + - name: remove outdated reviews + conditions: [] + actions: + dismiss_reviews: + changes_requested: False diff --git a/.pvsconfig b/.pvsconfig new file mode 100644 index 0000000000..9a56d49171 --- /dev/null +++ b/.pvsconfig @@ -0,0 +1 @@ +//-V::1042 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 91cda2fc51..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,118 +0,0 @@ -language: cpp - -git: - quiet: true - depth: 1 - -env: - global: - - COMMON_CMAKE_OPTIONS="-DEXIV2_ENABLE_WEBREADY=ON -DEXIV2_ENABLE_CURL=ON -DEXIV2_BUILD_UNIT_TESTS=ON -DEXIV2_ENABLE_BMFF=ON -DEXIV2_TEAM_WARNINGS_AS_ERRORS=ON -DCMAKE_INSTALL_PREFIX=install" - -matrix: - include: - - name: "Ubuntu 16.04 - gcc-5.4 (Release)" - os: linux - dist: xenial - sudo: required - compiler: gcc - env: - - BUILD_TYPE="Release" - - - name: "Ubuntu 16.04 - gcc-5.4 (Debug)" - os: linux - dist: xenial - sudo: required - compiler: gcc - env: - - BUILD_TYPE="Debug" - - - name: "Ubuntu 18.04 - gcc (Release)" - os: linux - dist: bionic - sudo: required - compiler: gcc - env: - - BUILD_TYPE="Release" - - - name: "Ubuntu 18.04 - gcc (Debug)" - os: linux - dist: bionic - sudo: required - compiler: gcc - env: - - BUILD_TYPE="Debug" - - - name: "Ubuntu 20.04 - gcc (Release)" - os: linux - dist: focal - sudo: required - compiler: gcc - env: - - BUILD_TYPE="Release" - - - name: "Ubuntu 20.04 - gcc (Debug)" - os: linux - dist: focal - sudo: required - compiler: gcc - env: - - BUILD_TYPE="Debug" - - - name: "Ubuntu 16.04 - gcc-5.4 with coverage" - os: linux - dist: xenial - sudo: required - compiler: gcc - env: - - WITH_COVERAGE=1 - - BUILD_TYPE="Release" - - - name: "Ubuntu 16.04 - gcc-5.4 with Valgrind" - os: linux - dist: xenial - sudo: required - compiler: gcc - env: - - WITH_VALGRIND=1 - - BUILD_TYPE="Release" - - - name: "Ubuntu 16.04 - gcc-5.4 with sanitizers" - os: linux - dist: xenial - sudo: required - compiler: gcc - env: - - WITH_SANITIZERS=1 - - BUILD_TYPE="Release" - - - name: "Ubuntu 16.04 - CLANG 7.0 (Release)" - os: linux - dist: xenial - sudo: required - compiler: clang - env: - - BUILD_TYPE="Release" - - - name: "Ubuntu 16.04 - CLANG 7.0 (Debug)" - os: linux - dist: xenial - sudo: required - compiler: clang - env: - - BUILD_TYPE="Debug" - - - name: "macOS 10.14 - XCode 11.3" - os: osx - osx_image: xcode11.3 - compiler: clang - env: - - BUILD_TYPE="Release" - -install: ./ci/install.sh -script: ./ci/run.sh - -cache: - ccache: true - directories: - - conan # Conan installation folder - - $HOME/conanData # Conan storage location diff --git a/CMakeLists.txt b/CMakeLists.txt index e03e840b47..c42a6265bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,152 +1,201 @@ -cmake_minimum_required( VERSION 3.3.2 ) - -project(exiv2 # use TWEAK to categorize the build - VERSION 0.27.4.2 # 0.27.4 = GM (tagged and released) - # 0.27.4.9 = 27.4.9 Development - # 0.27.4.00 = GM Preview - # 0.27.4.2 = RC2 (tagged and released) - # 0.27.4.20 = RC2 Preview - # 0.27.4.29 = RC2 Development - LANGUAGES CXX +# Minimum version imposed by Ubuntu:20.04 +cmake_minimum_required(VERSION 3.16.3) + +# use TWEAK to categorize the build: +# +# * 1.00.0 = GM (tagged and released) +# * 1.00.0.9 = 1.00.0 Development +# * 1.00.0.00 = GM Preview +# * 1.00.0.2 = RC2 (tagged and released) +# * 1.00.0.20 = RC2 Preview +# * 1.00.0.29 = RC2 Development +# +project( + exiv2 + VERSION 1.00.0.9 + DESCRIPTION "Exif/IPTC/Xmp C++ metadata library and tools plus ICC Profiles, Previews and more." + LANGUAGES C CXX ) -include(cmake/mainSetup.cmake REQUIRED) + +# Shared Object versioning (SemVer-like: must bump major on API breakage) +if(PROJECT_VERSION_MAJOR EQUAL 0) + # support legacy scheme (e.g. 0.27.x -> 27) + set(EXIV2LIB_SOVERSION ${PROJECT_VERSION_MINOR}) +else() + # restart from 30 + math(EXPR EXIV2LIB_SOVERSION "30 + (${PROJECT_VERSION_MAJOR} - 1)") +endif() + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +include(cmake/mainSetup.cmake) # options and their default values -option( BUILD_SHARED_LIBS "Build exiv2lib as a shared library" ON ) -option( EXIV2_ENABLE_XMP "Build with XMP metadata support" ON ) -option( EXIV2_ENABLE_EXTERNAL_XMP "Use external version of XMP" OFF ) -option( EXIV2_ENABLE_PNG "Build with png support (requires libz)" ON ) -option( EXIV2_ENABLE_NLS "Build native language support (requires gettext)" OFF ) -option( EXIV2_ENABLE_PRINTUCS2 "Build with Printucs2" ON ) -option( EXIV2_ENABLE_LENSDATA "Build including lens data" ON ) -option( EXIV2_ENABLE_VIDEO "Build video support into library" OFF ) -option( EXIV2_ENABLE_DYNAMIC_RUNTIME "Use dynamic runtime (used for static libs)" ON ) -option( EXIV2_ENABLE_WIN_UNICODE "Use Unicode paths (wstring) on Windows" OFF ) -option( EXIV2_ENABLE_WEBREADY "Build webready support into library" OFF ) -option( EXIV2_ENABLE_CURL "USE Libcurl for HttpIo (WEBREADY)" OFF ) -option( EXIV2_ENABLE_SSH "USE Libssh for SshIo (WEBREADY)" OFF ) -option( EXIV2_ENABLE_BMFF "Build with BMFF support" OFF ) - -option( EXIV2_BUILD_SAMPLES "Build sample applications" ON ) -option( EXIV2_BUILD_EXIV2_COMMAND "Build exiv2 command-line executable" ON ) -option( EXIV2_BUILD_UNIT_TESTS "Build unit tests" OFF ) -option( EXIV2_BUILD_DOC "Add 'doc' target to generate documentation" OFF ) +option(BUILD_SHARED_LIBS "Build exiv2lib as a shared library" ON) +option(EXIV2_ENABLE_XMP "Build with XMP metadata support" ON) +option(EXIV2_ENABLE_EXTERNAL_XMP "Use external version of XMP" OFF) +option(EXIV2_ENABLE_PNG "Build with PNG support (requires zlib)" ON) +option(EXIV2_ENABLE_NLS "Build native language support (requires gettext)" OFF) +option(EXIV2_ENABLE_LENSDATA "Build including Nikon lens data" ON) +option(EXIV2_ENABLE_DYNAMIC_RUNTIME "Use dynamic runtime (used for static libs)" ON) +option(EXIV2_ENABLE_WEBREADY "Build webready support into library" OFF) +option(EXIV2_ENABLE_CURL "Use libcurl for HttpIo (WEBREADY)" OFF) +option(EXIV2_ENABLE_BMFF "Build with BMFF support" ON) +option(EXIV2_ENABLE_BROTLI "Use Brotli for JPEG XL compressed boxes (BMFF)" ON) +option(EXIV2_ENABLE_VIDEO "Build with video support" ON) +option(EXIV2_ENABLE_INIH "Use inih library" ON) +option(EXIV2_ENABLE_FILESYSTEM_ACCESS "Build with filesystem access" ON) + +option(EXIV2_BUILD_SAMPLES "Build sample applications" OFF) +option(EXIV2_BUILD_EXIV2_COMMAND "Build exiv2 command-line executable" ON) +option(EXIV2_BUILD_UNIT_TESTS "Build unit tests" OFF) +option(EXIV2_BUILD_FUZZ_TESTS "Build fuzz tests (libFuzzer)" OFF) +option(EXIV2_BUILD_DOC "Add 'doc' target to generate documentation" OFF) # Only intended to be used by Exiv2 developers/contributors -option( EXIV2_TEAM_EXTRA_WARNINGS "Add more sanity checks using compiler flags" OFF ) -option( EXIV2_TEAM_WARNINGS_AS_ERRORS "Treat warnings as errors" OFF ) -option( EXIV2_TEAM_USE_SANITIZERS "Enable ASAN and UBSAN when available" OFF ) -option( EXIV2_TEAM_PACKAGING "Additional stuff for generating packages" OFF ) -set(EXTRA_COMPILE_FLAGS " ") +option(EXIV2_TEAM_EXTRA_WARNINGS "Add more sanity checks using compiler flags" OFF) +option(EXIV2_TEAM_WARNINGS_AS_ERRORS "Treat warnings as errors" OFF) +option(EXIV2_TEAM_USE_SANITIZERS "Enable ASAN and UBSAN when available" OFF) -mark_as_advanced( - EXIV2_TEAM_EXTRA_WARNINGS - EXIV2_TEAM_WARNINGS_AS_ERRORS - EXIV2_ENABLE_EXTERNAL_XMP - EXTRA_COMPILE_FLAGS - EXIV2_TEAM_USE_SANITIZERS -) +# The EXIV2_TEAM_OSS_FUZZ option is used by the OSS-Fuzz build script: +# +# * https://github.com/google/oss-fuzz/tree/master/projects/exiv2/build.sh +# +option(EXIV2_TEAM_OSS_FUZZ "Build config for OSS-Fuzz" OFF) -option( BUILD_WITH_CCACHE "Use ccache to speed up compilations" OFF ) -option( BUILD_WITH_COVERAGE "Add compiler flags to generate coverage stats" OFF ) +option(EXIV2_TEAM_PACKAGING "Additional stuff for generating packages" OFF) +set(EXTRA_COMPILE_FLAGS " ") -set( PACKAGE_BUGREPORT "http://github.com/exiv2/exiv2" ) -set( PACKAGE_URL "https://exiv2.org") -set( PROJECT_DESCRIPTION "Exif/IPTC/Xmp C++ metadata library and tools plus ICC Profiles, Previews and more.") +mark_as_advanced(EXIV2_TEAM_EXTRA_WARNINGS EXIV2_TEAM_WARNINGS_AS_ERRORS EXIV2_ENABLE_EXTERNAL_XMP EXTRA_COMPILE_FLAGS EXIV2_TEAM_USE_SANITIZERS) -if ( EXIV2_ENABLE_EXTERNAL_XMP ) - set(EXIV2_ENABLE_XMP OFF) -endif() +option(BUILD_WITH_STACK_PROTECTOR "Build with stack protector" ON) +option(BUILD_WITH_CCACHE "Use ccache to speed up compilations" OFF) +option(BUILD_WITH_COVERAGE "Add compiler flags to generate coverage stats" OFF) +include(cmake/gcovr.cmake REQUIRED) + +set(PACKAGE_URL "https://exiv2.org") -if( EXIV2_BUILD_UNIT_TESTS ) - set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) # Requires CMake 3.3.3 +if(EXIV2_ENABLE_EXTERNAL_XMP) + set(EXIV2_ENABLE_XMP OFF) endif() -include(cmake/findDependencies.cmake REQUIRED) -include(cmake/compilerFlags.cmake REQUIRED) +include(cmake/findDependencies.cmake REQUIRED) +include(cmake/compilerFlags.cmake REQUIRED) include(cmake/generateConfigFile.cmake REQUIRED) -if (EXIV2_BUILD_DOC) - include(cmake/generateDoc.cmake REQUIRED) - generate_documentation("${PROJECT_SOURCE_DIR}/cmake/Doxyfile.in") +if(EXIV2_BUILD_DOC) + include(cmake/generateDoc.cmake REQUIRED) + generate_documentation("${PROJECT_SOURCE_DIR}/cmake/Doxyfile.in") endif() +include_directories(${CMAKE_BINARY_DIR}) # Make the exv_conf.h file visible for the full project -include_directories(${CMAKE_BINARY_DIR}) # Make the exv_conf.h file visible for the full project +if(NOT EXV_HAVE_STD_FORMAT) + find_package(fmt "5.0.0" REQUIRED) +endif() -if( EXIV2_ENABLE_XMP ) - add_subdirectory( xmpsdk ) +if(EXIV2_ENABLE_XMP) + add_subdirectory(xmpsdk) endif() include(cmake/compilerFlagsExiv2.cmake REQUIRED) -add_subdirectory( include ) -add_subdirectory( src ) +add_subdirectory(src) -if( EXIV2_BUILD_UNIT_TESTS ) - add_subdirectory ( unitTests ) - add_custom_target(unit_test - COMMAND env EXIV2_BINDIR="${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" python3 runner.py bash_tests/unit_test.py - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests" - ) +if(BUILD_TESTING AND EXIV2_BUILD_UNIT_TESTS) + set(EXIV2_ENABLE_FILESYSTEM_ACCESS ON) + add_subdirectory(unitTests) endif() -add_custom_target(version_test - COMMAND env EXIV2_BINDIR="${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" python3 runner.py bash_tests/version_test.py - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests" -) +if(EXIV2_BUILD_FUZZ_TESTS) + set(EXIV2_ENABLE_FILESYSTEM_ACCESS ON) + add_subdirectory(fuzz) +endif() + +if(EXIV2_BUILD_EXIV2_COMMAND) + add_subdirectory(app) + set(EXIV2_ENABLE_FILESYSTEM_ACCESS ON) + + if(EXIV2_BUILD_SAMPLES) + add_subdirectory(samples) + set(EXIV2_ENABLE_FILESYSTEM_ACCESS ON) + get_directory_property(SAMPLES DIRECTORY samples DEFINITION APPLICATIONS) -if( EXIV2_BUILD_SAMPLES ) - ## - # tests - if( EXIV2_BUILD_UNIT_TESTS ) - add_custom_target(tests - COMMAND env EXIV2_BINDIR="${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" python3 runner.py bash_tests/unit_test.py - COMMAND env EXIV2_BINDIR="${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" python3 runner.py bash_tests/testcases.py --verbose - COMMAND env EXIV2_BINDIR="${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" python3 runner.py - COMMAND env EXIV2_BINDIR="${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" python3 runner.py bash_tests/version_test.py - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests" - ) - else() - add_custom_target(tests - COMMAND env EXIV2_BINDIR="${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" python3 runner.py bash_tests/testcases.py --verbose - COMMAND env EXIV2_BINDIR="${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" python3 runner.py - COMMAND env EXIV2_BINDIR="${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" python3 runner.py bash_tests/version_test.py - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests" - ) + if(BUILD_TESTING AND Python3_Interpreter_FOUND) + add_test( + NAME bashTests + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests + COMMAND cmake -E env EXIV2_BINDIR=${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ${Python3_EXECUTABLE} runner.py --verbose bash_tests + ) endif() - add_custom_target(python_tests - COMMAND env EXIV2_BINDIR="${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" python3 runner.py - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests" + endif() + + if(BUILD_TESTING AND Python3_Interpreter_FOUND) + add_test( + NAME bugfixTests + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests + COMMAND cmake -E env EXIV2_BINDIR=${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ${Python3_EXECUTABLE} runner.py --verbose bugfixes ) - add_custom_target(bash_tests - COMMAND env EXIV2_BINDIR="${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" python3 runner.py bash_tests/testcases.py --verbose - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests" + add_test( + NAME lensTests + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests + COMMAND cmake -E env EXIV2_BINDIR=${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ${Python3_EXECUTABLE} runner.py --verbose lens_tests ) - add_subdirectory( samples ) - get_directory_property(SAMPLES DIRECTORY samples DEFINITION APPLICATIONS) - add_dependencies(tests exiv2lib exiv2 ${SAMPLES}) + # Repeat lens test with a DE locale that uses "," as float delimiter + # to check that all float conversions are done independently of locale + # See GitHub issue #2746 + add_test( + NAME lensTestsLocaleDE + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests + COMMAND cmake -E env LC_ALL=de_DE.UTF-8 EXIV2_BINDIR=${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ${Python3_EXECUTABLE} runner.py --verbose lens_tests + ) + add_test( + NAME tiffTests + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests + COMMAND cmake -E env EXIV2_BINDIR=${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ${Python3_EXECUTABLE} runner.py --verbose tiff_test + ) + add_test( + NAME versionTests + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests + COMMAND cmake -E env EXIV2_BINDIR=${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ${Python3_EXECUTABLE} runner.py --verbose bash_tests/version_test.py + ) + add_test( + NAME regressionTests + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests + COMMAND cmake -E env EXIV2_BINDIR=${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ${Python3_EXECUTABLE} runner.py --verbose regression_tests + ) + endif() endif() -if( EXIV2_ENABLE_NLS ) - add_subdirectory( po ) +if(EXIV2_ENABLE_NLS) + add_subdirectory(po) endif() -if (EXIV2_TEAM_PACKAGING) - include(cmake/packaging.cmake) +if(EXIV2_TEAM_PACKAGING) + include(cmake/packaging.cmake) endif() -join_paths(libdir_for_pc_file "\${prefix}" "${CMAKE_INSTALL_LIBDIR}") -join_paths(includedir_for_pc_file "\${prefix}" "${CMAKE_INSTALL_INCLUDEDIR}") +# Handle both relative and absolute paths (e.g. NixOS) for a relocatable package +if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}") + set(EXIV2_LIBDIR "${CMAKE_INSTALL_LIBDIR}") +else() + join_paths(EXIV2_LIBDIR "\${exec_prefix}" "${CMAKE_INSTALL_LIBDIR}") +endif() +if(IS_ABSOLUTE "${CMAKE_INSTALL_INCLUDEDIR}") + set(EXIV2_INCLUDEDIR "${CMAKE_INSTALL_INCLUDEDIR}") +else() + join_paths(EXIV2_INCLUDEDIR "\${prefix}" "${CMAKE_INSTALL_INCLUDEDIR}") +endif() configure_file(cmake/exiv2.pc.in exiv2.pc @ONLY) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/exiv2.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) # ****************************************************************************** # Man page -install( FILES ${PROJECT_SOURCE_DIR}/man/man1/exiv2.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 ) - +install(FILES ${PROJECT_SOURCE_DIR}/man/man1/exiv2.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) include(cmake/printSummary.cmake) # That's all Folks! -## +# diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000000..6daa864754 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,151 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "base_ninja", + "description": "Base preset to use ninja as generator", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-${presetName}", + "installDir": "${sourceDir}/build-${presetName}/install", + "cacheVariables": { + "BUILD_SHARED_LIBS": true, + "CONAN_AUTO_INSTALL": true, + "EXIV2_BUILD_SAMPLES": true, + "EXIV2_ENABLE_WEBREADY": true, + "EXIV2_ENABLE_CURL": true, + "EXIV2_ENABLE_PNG": true, + "EXIV2_ENABLE_BMFF": true, + "EXIV2_BUILD_UNIT_TESTS": true, + "EXIV2_TEAM_WARNINGS_AS_ERRORS": true, + "EXIV2_ENABLE_NLS": false, + "EXIV2_ENABLE_VIDEO": true + } + }, + { + "name": "base_windows", + "description": "Base preset for Windows (specially useful for CI jobs)", + "displayName": "Base preset for Windows (specially useful for CI jobs)", + "inherits": "base_ninja", + "condition": { + "type": "matches", + "string": "${hostSystemName}", + "regex": "Windows|CYGWIN.*|MSYS.*" + } + }, + { + "name": "base_linux", + "description": "Base preset for Linux", + "displayName": "Base preset for Linux with default compiler: GCC (specially useful for CI jobs)", + "inherits": "base_ninja", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "cacheVariables": { + "BUILD_WITH_CCACHE": true + } + }, + { + "name": "base_mac", + "description": "Base preset for macOS (no conan usage)", + "displayName": "Base preset for macOS with default compiler: AppleClang (specially useful for CI jobs)", + "inherits": "base_ninja", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CONAN_AUTO_INSTALL": false + } + }, + + { + "name": "msvc", + "displayName": "Visual Studio cl toolchain (also usable from VS Code)", + "inherits": "base_windows", + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_C_COMPILER": "cl.exe", + "CMAKE_CXX_COMPILER": "cl.exe", + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "win-debug", + "displayName": "Windows Debug with configured architecture", + "description": "Sets Debug build type with the preloaded Visual Studio Environment", + "inherits": "base_windows", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "win-release", + "displayName": "Windows Release with configured architecture", + "description": "Sets Release build type with the preloaded Visual Studio Environment", + "inherits": "base_windows", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "linux-debug", + "displayName": "Linux Debug (Ninja Generator) with default architecture", + "inherits": "base_linux", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "linux-release", + "displayName": "Linux Release (Ninja Generator) with default architecture", + "inherits": "base_linux", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } + }, + { + "name": "linux-debug-NoConan", + "displayName": "Same as linux-debug but without automatic conan execution", + "inherits": "linux-debug", + "cacheVariables": { "CONAN_AUTO_INSTALL": false } + }, + { + "name": "linux-release-NoConan", + "displayName": "Same as linux-release but without automatic conan execution", + "inherits": "linux-release", + "cacheVariables": { "CONAN_AUTO_INSTALL": false } + }, + { + "name": "linux-coverage", + "displayName": "Same as linux-debug-NoConan with coverage enabled", + "inherits": "linux-debug-NoConan", + "cacheVariables": { "BUILD_WITH_COVERAGE": true } + }, + { + "name": "linux-sanitizers", + "displayName": "Same as linux-debug-NoConan with sanitizers enabled", + "inherits": "linux-debug-NoConan", + "cacheVariables": { "EXIV2_TEAM_USE_SANITIZERS": true } + }, + { + "name": "linux-all", + "displayName": "Same as linux-release-NoConan and with rest of things enabled (doc + NLS)", + "description": "requires installation of packages: doxygen graphviz gettext", + "inherits": "linux-release-NoConan", + "cacheVariables": { + "EXIV2_ENABLE_NLS": true, + "EXIV2_ENABLE_VIDEO": false, + "EXIV2_BUILD_DOC": true + } + } + + ] +} diff --git a/CODING_GUIDELINES.md b/CODING_GUIDELINES.md index 1f1ce3c6ff..e8e921287d 100644 --- a/CODING_GUIDELINES.md +++ b/CODING_GUIDELINES.md @@ -18,7 +18,7 @@ Coding Guidelines - All new code that is added must be resistant to integer overflows, thus if you multiply, add, subtract, divide or bitshift integers you must ensure that no overflow can occur. Please keep in mind that signed integer overflow is undefined behavior, thus you must check for overflows before performing the arithmetic operation, otherwise the compiler is free to optimize your check after the overflow away (this has happened already). - All new code must be resistant to buffer overflows. Thus before you access arrays a range check must be performed. - Distrust any data that you extract from images or from external sources. E.g. if the metadata of an image gives you an offset of another information inside that file, do not assume that this offset will not result in an out off bounds read. -- New code must not assume the endianes and the word size of the system it is being run on. I.e. don't assume that `sizeof(int) = 8` or that the following will work: +- New code must not assume the endianness and the word size of the system it is being run on. I.e. don't assume that `sizeof(int) = 8` or that the following will work: ```cpp const uint32_t some_var = get_var(); const uint16_t lower_2_bytes = (const uint16_t*) &some_var; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68d5b3ae3b..244dac31ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ We welcome any help, for example contributing lens data (images), code contribut Code contributions can be performed via *pull requests* (PR) on GitHub (if you cannot or do not want to use GitHub, see [3. Contributing code via email](#3-contributing-code-via-email)). For this to work you first need to [create a user account on GitHub](https://help.github.com/articles/signing-up-for-a-new-github-account/) if you don't already have one. -A pull request should preferable contain only one new feature or bug fix etc. Since it is not uncommon to work on several PRs at the same time +A pull request should preferable contain only one new feature, bug fix, etc. Since it is not uncommon to work on several PRs at the same time it is recommended to create a new _branch_ for each PR. In this way PRs can easily be separated and the review and merge process becomes cleaner. As a rule-of-thumb: @@ -31,7 +31,7 @@ As a rule-of-thumb: See the [GIT_GUIDELINES.md](git_guidelines.md) file for a more detailed description of the git workflow. Below we outline the recommended steps in the code contribution workflow. We use `your-username` to refer to your username on GitHub, `exiv2_upstream` is used when we -set the upstream remote repository for Exiv2 (we could have picked any name by try to avoid already used names like, in particular, `origin` and `master`), and +set the upstream remote repository for Exiv2 (we could have picked any name but try to avoid already used names like, in particular, `origin` and `main`), and we use the name `my-new-feature` for the branch that we create (e.g., the branch name should reflect the code change being made). **Important**: If your PR lives for a long time, then don't press the button _Update branch_ in the Pull Request view, instead follow the steps below, as @@ -59,34 +59,38 @@ Once you have a GitHub login: origin https://github.com/your-username/exiv2.git (fetch) origin https://github.com/your-username/exiv2.git (push) -4. Next, create a branch for your PR from `exiv2_upstream/master` (which we also need to fetch first): +4. Next, create a branch for your PR from `exiv2_upstream/main` (which we also need to fetch first): - $ git fetch exiv2_upstream master - $ git checkout -b my-new-feature exiv2_upstream/master --no-track + $ git fetch exiv2_upstream main + $ git checkout -b my-new-feature exiv2_upstream/main --no-track - NB: This is an important step to avoid draging in old commits! + NB: This is an important step to avoid dragging in old commits! 5. Configure the project and check that it builds (if not, please report a bug): $ rm -rf build - $ mkdir build && cd build - $ cmake -DCMAKE_BUILD_TYPE=Release .. - $ make + $ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + $ cmake --build build --parallel 6. Now, make your change(s), add tests for your changes, and commit each change: ... - + $ git commit -m "Commit message 1" - + ... - + $ git commit -m "Commit message 2" + Please keep in mind that the project has a Continuous Integration job to check that your new code is satisfying the + format defined in the file `.clang-format`. Use your preferred text editor, IDE or method to make sure your code is + properly formatted before creating the PR. + + Note: You can use the script `contrib/scripts/clangFormatWholeProject.sh` to format the whole project. + 7. Make sure the tests pass: - $ make tests # Integration tests - $./bin/unit_tests # Unit tests + $ ctest Exiv2's (new) test system is described in more detail in the [doc.md](tests/doc.md) and [writing_tests.md](tests/writing_tests.md) files, and a description of the old test system can be found in the Redmine wiki: [How do I run the test suite for Exiv2](http://dev.exiv2.org/projects/exiv2/wiki/How_do_I_run_the_test_suite_for_Exiv2) @@ -107,9 +111,9 @@ Once you have a GitHub login: $ git checkout my-new-feature - And rebase it on top of master: + And rebase it on top of main: - $ git pull --rebase exiv2_upstream master + $ git pull --rebase exiv2_upstream main When you perform a rebase the commit history is rewritten and, therefore, the next time you try to push your branch to your fork repository you will need to use the `--force-with-lease` option: diff --git a/GIT_GUIDELINES.md b/GIT_GUIDELINES.md index 020a365b60..aab3605892 100644 --- a/GIT_GUIDELINES.md +++ b/GIT_GUIDELINES.md @@ -16,7 +16,7 @@ A commit message can look like this: - Specify concrete ubuntu and mac versions - Use latest conan version - Fix the profiles for linux and mac -- Use new version of expat (avilable in conan-center) +- Use new version of expat (available in conan-center) - Install urllib3 as suggested in python guidelines - Use virtualenv with python3 ``` @@ -77,7 +77,7 @@ We can summarize this in the following guidelines: - Every commit should keep the code base in a buildable state. The test suite needn't pass on every commit, but must pass before being merged into - `master`. + `main`. These are however not strict rules and it always depends on the case. If in doubt: ask. @@ -89,7 +89,7 @@ We prefer to keep the git log nearly linear with the individual pull requests still visible, since they usually form one logical unit. It should look roughly like this: ``` -* 9f74f247 Merge pull request #227 from frli8848/master +* 9f74f247 Merge pull request #227 from frli8848/main |\ | * 73ac02d7 Added test for Sigma lenses | * fc8b45dd Added the Sigma 120-300mm F2.8 DG OS HSM | S for Nikon mount. @@ -141,61 +141,61 @@ rather complicated logs, like this: Instead of using the `Update Branch` button use `git pull --rebase`. For the following example, we'll assume that we are working in a branch called -`feature_xyz` that should be merged into the branch `master`. Furthermore the +`feature_xyz` that should be merged into the branch `main`. Furthermore the remote `origin` is a fork of exiv2 and the remote `upstream` is the "official" exiv2 repository. -Before we start working, the `master` branch looks like this: +Before we start working, the `main` branch looks like this: ``` -$ git log master --oneline --graph -* efee9a2b (master) Merge pull request #something +$ git log main --oneline --graph +* efee9a2b (main) Merge pull request #something |\ -| * ead7f309 A commit on master +| * ead7f309 A commit on main |/ * 55001c8d Merge pull request #something else ``` -We create a new branch `feature_xyz` based on `master`, create two new commits -`My commit 1` and `My commit 2` and submit a pull request into master. The log +We create a new branch `feature_xyz` based on `main`, create two new commits +`My commit 1` and `My commit 2` and submit a pull request into `main`. The log of the branch `feature_xyz` now looks like this: ``` $ git log feature_xyz --oneline --graph * 893fffa5 (HEAD -> feature_xyz) My commit 2 * a2a22fb9 My commit 1 -* efee9a2b (master) Merge pull request #something +* efee9a2b (main) Merge pull request #something |\ -| * ead7f309 A commit on master +| * ead7f309 A commit on main |/ * 55001c8d Merge pull request #something else ``` -If now new commits are pushed to `master`, resulting in this log: +If now new commits are pushed to `main`, resulting in this log: ``` -$ git log master --oneline --graph -* 0d636cc9 (HEAD -> master) Hotfix for issue #something completely different +$ git log main --oneline --graph +* 0d636cc9 (HEAD -> main) Hotfix for issue #something completely different * efee9a2b Merge pull request #something |\ -| * ead7f309 A commit on master +| * ead7f309 A commit on main |/ * 55001c8d Merge pull request #something else ``` -then the branch `feature_xyz` is out of date with `master`, because it lacks the +then the branch `feature_xyz` is out of date with `main`, because it lacks the commit `0d636cc9`. We could now merge both branches (via the cli or GitHub's `Update Branch` button), but that will result in a messy history. Thus **don't** do it! If you do it, you'll have to remove the merge commits manually. -Instead run: `git pull --rebase upstream master` in the `feature_xyz` -branch. Git will pull the new commit `0d636cc9` from master into your branch +Instead run: `git pull --rebase upstream main` in the `feature_xyz` +branch. Git will pull the new commit `0d636cc9` from main into your branch `feature_xyz` and apply the two commits `My commit 1` and `My commit 2` on top of it: ``` $ git log feature_xyz --oneline --graph * 22a7a8c2 (HEAD -> feature_xyz) My commit 2 * efe2ccdc My commit 1 -* 0d636cc9 (master) Hotfix for issue #something completely different +* 0d636cc9 (main) Hotfix for issue #something completely different * efee9a2b Merge pull request #something |\ -| * ead7f309 A commit on master +| * ead7f309 A commit on main |/ * 55001c8d Merge pull request #something else ``` @@ -208,20 +208,20 @@ changes via `git push --force` next time you push your changes upstream. Most pull requests should be merged by creating a merge commit (the default on GitHub). Small pull requests (= only one can commit) can be rebased on top of -master. +`main`. ## Branches and tags -- The `master` branch is the current "main" development branch. It is protected +- The `main` branch is the current "main" development branch. It is protected so that changes can be only included via reviewed pull requests. New releases - are made by tagging a specific commit on `master`. + are made by tagging a specific commit on `main`. - Releases are tagged with a tag of the form `v$major.$minor`. The tag is not changed when changes are backported. - For each release a branch of the form `$major.$minor` should be created to - store backported changes. It should be branched of from `master` at the commit + store backported changes. It should be branched of from `main` at the commit which was tagged with `v$major.$minor`. - All other branches are development branches for pull requests, experiments, diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000..a02bc95245 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,17 @@ +This program is part of the Exiv2 distribution. +Copyright (C) 2004-2022 Exiv2 authors + +This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +--------------------------------------------------------------------------- +Note: +Individual files contain the following tag instead of the full license text. + + SPDX-License-Identifier: GPL-2.0-or-later + +This enables machine processing of license information based on the SPDX +License Identifiers that are here available: http://spdx.org/licenses/ diff --git a/README-CONAN.md b/README-CONAN.md index 7c6a7fe582..bc0c40d637 100644 --- a/README-CONAN.md +++ b/README-CONAN.md @@ -1,7 +1,3 @@ -| Travis | AppVeyor | GitLab| Codecov| Repology| Chat | -|:-------------:|:-------------:|:-----:|:------:|:-------:|:----:| -| [![Build Status](https://travis-ci.org/Exiv2/exiv2.svg?branch=0.27-maintenance)](https://travis-ci.org/Exiv2/exiv2) | [![Build status](https://ci.appveyor.com/api/projects/status/d6vxf2n0cp3v88al/branch/0.27-maintenance?svg=true)](https://ci.appveyor.com/project/piponazo/exiv2-wutfp/branch/0.27-maintenance) | [![pipeline status](https://gitlab.com/D4N/exiv2/badges/0.27-maintenance/pipeline.svg)](https://gitlab.com/D4N/exiv2/commits/0.27-maintenance) | [![codecov](https://codecov.io/gh/Exiv2/exiv2/branch/0.27-maintenance/graph/badge.svg)](https://codecov.io/gh/Exiv2/exiv2) | [![Packaging status](https://repology.org/badge/tiny-repos/exiv2.svg)](https://repology.org/metapackage/exiv2/versions) | [![#exiv2-chat on matrix.org](matrix-standard-vector-logo-xs.png)](https://matrix.to/#/#exiv2-chat:matrix.org) | - ![Exiv2](exiv2.png) # Building Exiv2 and dependencies with conan @@ -48,6 +44,7 @@ The build procedures for those platforms are discussed here: See [README.md](REA # 1 Step by Step Guide + ##### 1.1) Install conan: ```bash @@ -63,6 +60,7 @@ $ pip install conan --upgrade ``` + ##### 1.2) Test conan installation ```bash @@ -71,6 +69,7 @@ Conan version 1.23.0 ``` + ##### 1.3) Create a build directory Create a build directory and run the conan commands: @@ -99,26 +98,27 @@ os_build=Windows [env] ``` -_Profiles for Visual Studio are discussed in detail here: [Visual Studio Notes](#2-2)__ +_Profiles for Visual Studio are discussed in detail here: [Visual Studio Notes](#2-2)_ + ##### 1.4) Build dependencies, create build environment, build and test -| | Build Steps | Linux and macOS | Visual Studio | -|:-- |:--------------|--------------------------------|------------------------------| +| | Build Steps | Linux and macOS | Visual Studio | +|:-- |:-------------------------------------------------------------------------|-----------------------|------------------------------| | _**1**_ | Get conan to fetch dependencies

The output can be quite
long as conan downloads and/or builds
zlib, expat, curl and other dependencies.| $ conan install ..
     --build missing | c:\\..\\build> conan install .. --build missing
    --profile msvc2019Release64 | -| _**2**_ | Get cmake to generate
makefiles or sln/vcxproj | $ cmake .. | c:\\..\\build> cmake .. -G "Visual Studio 16 2019" -| _**3**_ | Build | $ cmake --build . | c:\\..\\build> cmake --build . --config Release
You may prefer to open exiv2.sln and build using the IDE. | -| _**4**_ | Optionally Run Test Suite | $ make tests | c:\\..\\build> cmake --build . --config Release --target tests
[README.md](README.md) | - - +| _**2**_ | Get cmake to generate
makefiles or sln/vcxproj | $ cmake .. | c:\\..\\build> cmake .. -G "Visual Studio 16 2019" +| _**3**_ | Build | $ cmake --build . | c:\\..\\build> cmake --build . --config Release
You may prefer to open exiv2.sln and build using the IDE. | +| _**4**_ | Optionally Run Test Suite
Test documentation: [README.md](README.md) | $ ctest | c:\\..\\build> ctest -C Release | [TOC](#TOC) + ## 2) Platform Notes + ### 2.1) Linux Notes ##### Default Profile @@ -169,6 +169,7 @@ algorithms when bringing the Exiv2 dependencies with conan, this might indicate [TOC](#TOC) + ### 2.2) Visual Studio Notes We recommend that you install python as discussed here: [https://github.com/Exiv2/exiv2/pull/1403#issuecomment-731836146](https://github.com/Exiv2/exiv2/pull/1403#issuecomment-731836146) @@ -177,8 +178,8 @@ We recommend that you install python as discussed here: [https://github.com/Exi Exiv2 v0.27 can be built with VS 2008, 2010, 2012, 2013, 2015 , 2017 and 2019. -Exiv2 v0.28 is being "modernised" to C++11 and will not support C++98. -We don't expect Exiv2 v0.28 to build with VS versions earlier than VS 2015. +Exiv2 v1.0 is being "modernised" to C++11 and will not support C++98. +We don't expect Exiv2 v1.0 to build with VS versions earlier than VS 2015. You create profiles in %HOMEPATH%\.conan\profiles with a text editor. For your convenience, you'll find profiles in `\cmake\msvc_conan_profiles`. @@ -297,6 +298,7 @@ $ cmake --build . --config Release ## 3 Conan Architecture + ##### 3.1) conanfile.py In the root level of the **Exiv2** repository, the file `conanfile.py` defines C/C++ dependencies with the syntax: `Library/version@user/channel` @@ -309,6 +311,7 @@ self.requires('self.requires('zlib/1.2.11@conan/stable')') [TOC](#TOC) + ##### 3.2) Conan _**Recipes**_ Conan searches remote servers for a _**recipe**_ to build a dependency. @@ -358,6 +361,7 @@ Existing packages for recipe zlib/1.2.11@conan/stable: [TOC](#TOC) + ##### 3.3) Conan server search path Conan searches remote servers for a _**recipe**_ to build the dependency. You can list them with the command: @@ -374,6 +378,7 @@ $ conan remote add conan-piponazo https://api.bintray.com/conan/piponazo/piponaz [TOC](#TOC) + ##### 3.4) Configuring conan on your machine Conan stores its configuration and local builds in the directory ~/.conan (%HOMEPATH%\\.conan on Windows). @@ -387,6 +392,7 @@ $HOME/.conan/data Dependencies are built/stored in this directory [TOC](#TOC) + ##### 3.5) Running `conan install` for the first time The first time you run `$ conan install`, it will auto-detect your configuration and store a default profile in the file @@ -511,6 +517,7 @@ Indicating that the packages were found in the local cache. [TOC](#TOC) + ## 4 Building Exiv2 with Adobe XMPsdk 2016 With Exiv2 v0.27, you can build Exiv2 with Adobe XMPsdk 2016 on Linux/GCC, Mac/clang and Visual Studio 2017. @@ -521,6 +528,7 @@ library can be used by the application and Exiv2. The Adobe XMPsdk can be built To build Exiv2 with Adobe XMPsdk 2016, perform steps 1.1, 1.2 and 1.3 described above, then perform the following: + ##### 4.1) Add a remote directory to conan's recipe search path By default, conan knows about several public conan repositories. Exiv2 requires @@ -531,6 +539,7 @@ $ conan remote add conan-piponazo https://api.bintray.com/conan/piponazo/piponaz ``` + ##### 4.2) Build dependencies and install conan artefacts in your build directory ```bash @@ -538,6 +547,7 @@ $ conan install .. --options xmp=True --build missing ``` + ##### 4.3) Execute cmake to generate build files for your environment: You must tell CMake to link Adobe's library: @@ -552,6 +562,7 @@ $ cmake .. -DEXIV2_ENABLE_EXTERNAL_XMP=On -G Xcode ``` + ##### 4.4) Build Exiv2 and link Adobe XMPsdk library ```bash @@ -560,17 +571,18 @@ $ cmake --build . --config Release [TOC](#TOC) + ## 5 Webready Support Exiv2 can perform I/O using internet protocols such as http, https and ftp. -The feature is disabled by default. You will need to instruct conan to build/download necessary libraries (curl, openssl and libssh) and tell CMake to link to the libraries. +The feature is disabled by default. You will need to instruct conan to build/download necessary libraries (curl and openssl) and tell CMake to link to the libraries. ```bash $ conan install .. --options webready=True -$ cmake -DEXIV2_ENABLE_WEBREADY=ON -DEXIV2_ENABLE_CURL=ON -DEXIV2_ENABLE_SSH=ON .. +$ cmake -DEXIV2_ENABLE_WEBREADY=ON -DEXIV2_ENABLE_CURL=ON .. ``` [TOC](#TOC) -Written by Robin Mills
robin@clanmills.com
Updated: 2021-03-18 +Written by Robin Mills
robin@clanmills.com
Updated: 2021-12-17 diff --git a/README-SAMPLES.md b/README-SAMPLES.md index eea883f34f..dcb56b022c 100644 --- a/README-SAMPLES.md +++ b/README-SAMPLES.md @@ -21,9 +21,9 @@ The following programs are build and installed in /usr/local/bin. | _**addmoddel**_ | Demonstrates Exiv2 library APIs to add, modify or delete metadata | [addmoddel](#addmoddel) | [addmoddel.cpp](samples/addmoddel.cpp) | | _**exifcomment**_ | Set Exif.Photo.UserComment in an image | [exifcomment](#exifcomment) | [exifcomment.cpp](samples/exifcomment.cpp) | | _**exifdata**_ | Prints _**Exif**_ metadata in different formats in an image | [exifdata](#exifdata) | [exifdata.cpp](samples/exifdata.cpp) | -| _**exifprint**_ | Print _**Exif**_ metadata in images
Miscelleous other features | [exifprint](#exifprint)| [exifprint.cpp](samples/exifprint.cpp) | +| _**exifprint**_ | Print _**Exif**_ metadata in images
Miscellaneous other features | [exifprint](#exifprint)| [exifprint.cpp](samples/exifprint.cpp) | | _**exifvalue**_ | Prints the value of a single _**Exif**_ tag in a file | [exifvalue](#exifvalue) | [exifvalue.cpp](samples/exifvalue.cpp) | -| _**exiv2**_ | Command line utility to read, write, delete and modify Exif, IPTC, XMP and ICC image metadata.
This is the primary test tool used by Team Exiv2 and can exercise almost all code in the library. Due to the extensive capability of this utility, the APIs used are usually less obvious for casual code inspection. | [https://exiv2.org/manpage.html](https://exiv2.org/manpage.html)
[https://exiv2.org/sample.html](https://exiv2.org/sample.html) | | +| _**exiv2**_ | Utility to read and write image metadata, including Exif, IPTC, XMP, image comments, ICC Profile, thumbnails, image previews and many vendor makernote tags.
This is the primary test tool used by Team Exiv2 and can exercise almost all code in the library. Due to the extensive capability of this utility, the APIs used are usually less obvious for casual code inspection. | [exiv2 manpage](exiv2.md)
[https://exiv2.org/sample.html](https://exiv2.org/sample.html) | | | _**exiv2json**_ | Extracts data from image in JSON format.
This program also contains a parser to recursively parse Xmp metadata into vectors and objects. | [exiv2json](#exiv2json) | [exiv2json.cpp](samples/exiv2json.cpp) | | _**geotag**_ | Reads GPX data and updates images with GPS Tags | [geotag](#geotag) | [geotag.cpp](samples/geotag.cpp) | | _**iptceasy**_ | Demonstrates read, set or modify IPTC metadata | [iptceasy](#iptceasy) | [iptceasy.cpp](samples/iptceasy.cpp) | @@ -61,7 +61,6 @@ As Exiv2 is open source, we publish all our materials. The following programs a | _**remotetest**_ | Tester application for testing remote i/o. | [remotetest](#remotetest) | | _**stringto-test**_ | Test conversions from string to long, float and Rational types. | [stringto-test](#stringto-test) | | _**tiff-test**_ | Simple TIFF write test | [tiff-test](#tiff-test) | -| _**werror-test**_ | Simple tests for the wide-string error class WError | [werror-test](#werror-test) | | _**write-test**_ | ExifData write unit tests | [write-test](#write-test) | | _**write2-test**_ | ExifData write unit tests for Exif data created from scratch | [write2-test](#write2-test) | | _**xmpparser-test**_ | Read an XMP packet from a file, parse and re-serialize it. | [xmpparser-test](#xmpparser-test)| @@ -112,18 +111,21 @@ This is a simple program to demonstrate dumping _**Exif**_ metadata in common fo #### exifprint ``` -Usage: exifprint [ path | --version | --version-test ] +Usage: exifprint [ [--lint] path | --version | --version-test ] ``` | Arguments | Description | |:-- |:--- | | path | Path to image | +| --lint path | Path to image. Type metadata test | | --version | Print version information from build | | --version-test | Tests Exiv2 VERSION API | This program demonstrates how to print _**Exif**_ metadata in an image. This program is also discussed in the platform ReadMe.txt file included in a build bundle. The option **--version** was added to enable the user to build a test application which dumps the build information. The option **--version-test** was added to test the macro EXIV2\_TEST\_VERSION() in **include/exiv2/version.hpp**. -There is one other unique feature of this program. It is the only test/sample program which can use the EXV\_UNICODE\_PATH build feature of Exiv2 on Windows. +You can process the metadata in two different ways. The default prints the metadata. The option --lint instructs exifprint to compare the type of the metadata to the standard. + +There is another unique feature of this program. It is the only test/sample program which can use the EXV\_UNICODE\_PATH build feature of Exiv2 on Windows. _Code: [exifprint.cpp](samples/exifprint.cpp)_ @@ -159,16 +161,20 @@ Option: all | exif | iptc | xmp | filesystem This program dumps metadata from an image in JSON format. _Code: [exiv2json.cpp](samples/exiv2json.cpp)_ -exiv2json has a recursive parser to encode XMP into Vectors and Objects. XMP data is XMP and can contain XMP `Bag` and `Seq` which are converted to JSON Objects and Arrays. Exiv2 presents data in the format: Family.Group.Tag. For XMP, results in "flat" output such such as: +exiv2json has a recursive parser to encode XMP into Vectors and Objects. XMP data is XMP and can contain XMP `Bag` and `Seq` which are converted to JSON Objects and Arrays. Exiv2 presents data in the format: [Family.Group.Tagname](exiv2.md#exiv2_key_syntax). For XMP, results in "flat" output such as: ``` -$ exiv2 -px ~/Stonehenge.jpg +$ curl --silent -O https://clanmills.com/Stonehenge.jpg +$ exiv2 --print x Stonehenge.jpg Xmp.xmp.Rating XmpText 1 0 Xmp.xmp.ModifyDate XmpText 25 2015-07-16T20:25:28+01:00 +Xmp.cm2e.Father XmpText 11 Robin Mills +Xmp.cm2e.Family XmpBag 0 Xmp.dc.description LangAlt 1 lang="x-default" Classic View +Xmp.dc.Family XmpBag 1 Robin ``` -exiv2json parses the Exiv2 'Family.Group.Tag' data and restores the structure of the original data in JSON. _Code: [exiv2json.cpp](samples/exiv2json.cpp)_ +exiv2json parses the Exiv2 [Family.Group.Tagname](exiv2.md#exiv2_key_syntax) data and restores the structure of the original data in JSON. _Code: [exiv2json.cpp](samples/exiv2json.cpp)_ ``` $ exiv2json -xmp http://clanmills.com/Stonehenge.jpg @@ -204,7 +210,7 @@ $ Usage: geotag {-help|-version|-dst|-dryrun|-ascii|-verbose|-adjust value|-tz value|-delta value}+ path+ ``` -Geotag reads one or more GPX files and adds GPS Tages to images. _Code: [geotag.cpp](samples/geotag.cpp)_ +Geotag reads one or more GPX files and adds GPS Tags to images. _Code: [geotag.cpp](samples/geotag.cpp)_ If the path is a directory, geotag will read all the files in the directory. It constructs a time dictionary of position data, then updates every image with GPS Tags. @@ -346,10 +352,16 @@ Conversion test driver #### easyaccess-test ``` -Usage: easyaccess-test file +Usage: ..\build\bin\easyaccess-test.exe file [category [category ...]] +Categories: Orientation | ISOspeed | DateTimeOriginal | FlashBias | ExposureMode | SceneMode | + MacroMode | ImageQuality | WhiteBalance | LensName | Saturation | Sharpness | + Contrast | SceneCaptureType | MeteringMode | Make | Model | ExposureTime | FNumber | + ShutterSpeed | Aperture | Brightness | ExposureBias | MaxAperture | SubjectDistance | + LightSource | Flash | SerialNumber | FocalLength | SubjectArea | FlashEnergy | + ExposureIndex | SensingMethod | AFpoint ``` -Sample program using high-level metadata access functions +Sample program using high-level metadata access functions. Without specification of a category, metadata for all categories are shown. [Sample](#TOC1) Programs [Test](#TOC2) Programs @@ -471,7 +483,7 @@ Test access to preview images #### remotetest ``` -Usage: remotetest remotetest file {--nocurl | --curl} +Usage: remotetest file {--nocurl | --curl} ``` Tester application for testing remote i/o. @@ -529,7 +541,8 @@ FlashDevice, 9, 0x0009, Nikon3, Exif.Nikon3.FlashDevice, Ascii, Flash de We can see those tags being used: ``` -$ exiv2 -pa --grep Nikon3 http://clanmills.com/Stonehenge.jpg +$ curl --silent -O https://clanmills.com/Stonehenge.jpg +$ exiv2 --print a --grep Nikon3 Stonehenge.jpg Exif.Nikon3.Version Undefined 4 2.11 Exif.Nikon3.ISOSpeed Short 2 200 ... @@ -539,7 +552,7 @@ This information is formatted (search Nikon (format 3) MakerNote Tags): [https:/ #### taglist all -These options are provided to list every Exif tag known to Exiv2. The option `all` prints Group.Name for every tag. The option `ALL` print Group.Name followed by the TagInfo for that tag. For example: +These options are provided to list every tag known to Exiv2. The option `all` prints the [Group.Tagnames](exiv2.md#exiv2_key_syntax) for every Exif tag. The option `ALL` prints the [Group.Tagnames](exiv2.md#exiv2_key_syntax) for every Exif tag, followed by the TagInfo for that tag. For example: ```bash $ taglist all | grep ISOSpeed$ @@ -591,18 +604,6 @@ Simple TIFF write test [Sample](#TOC1) Programs [Test](#TOC2) Programs -
- -#### werror-test - -``` -Usage: werror-test -``` - -Simple tests for the wide-string error class WError - -[Sample](#TOC1) Programs [Test](#TOC2) Programs -
#### write-test @@ -656,4 +657,4 @@ Read an XMP packet from a file, parse and re-serialize it. Robin Mills
robin@clanmills.com
-Revised: 2020-11-20 \ No newline at end of file +Revised: 2021-09-21 diff --git a/README-meson b/README-meson new file mode 100644 index 0000000000..54089b12e5 --- /dev/null +++ b/README-meson @@ -0,0 +1,10 @@ +meson build system + +This is a basic meson.build file intended to be used by meson projects that use +WrapDB: https://github.com/mesonbuild/wrapdb + +This can also be used as a test playground to test various options not exposed +by the CMake build, eg: iconv and intl on Windows. + +It is currently incomplete. Tests are not implemented yet. The library and +executable are. diff --git a/README.md b/README.md index ce6a2ba12a..116f63ff44 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,19 @@ -| Travis | AppVeyor | GitLab| Codecov| Repology| Chat | -|:-------------:|:-------------:|:-----:|:------:|:-------:|:----:| -| [![Build Status](https://travis-ci.org/Exiv2/exiv2.svg?branch=0.27-maintenance)](https://travis-ci.org/Exiv2/exiv2) | [![Build status](https://ci.appveyor.com/api/projects/status/d6vxf2n0cp3v88al/branch/0.27-maintenance?svg=true)](https://ci.appveyor.com/project/piponazo/exiv2-wutfp/branch/0.27-maintenance) | [![pipeline status](https://gitlab.com/D4N/exiv2/badges/0.27-maintenance/pipeline.svg)](https://gitlab.com/D4N/exiv2/commits/0.27-maintenance) | [![codecov](https://codecov.io/gh/Exiv2/exiv2/branch/0.27-maintenance/graph/badge.svg)](https://codecov.io/gh/Exiv2/exiv2) | [![Packaging status](https://repology.org/badge/tiny-repos/exiv2.svg)](https://repology.org/metapackage/exiv2/versions) | [![#exiv2-chat on matrix.org](matrix-standard-vector-logo-xs.png)](https://matrix.to/#/#exiv2-chat:matrix.org) | +| Codecov | OSS-Fuzz | Repology | Chat | +| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | +| [![codecov](https://codecov.io/gh/Exiv2/exiv2/branch/main/graph/badge.svg?token=O9G7Iswx26)](https://codecov.io/gh/Exiv2/exiv2) | [![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/exiv2.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:exiv2) | [![Packaging status](https://repology.org/badge/tiny-repos/exiv2.svg)](https://repology.org/metapackage/exiv2/versions) | [![#exiv2-chat on matrix.org](matrix-standard-vector-logo-xs.png)](https://matrix.to/#/#exiv2-chat:matrix.org) | + + + +| **CI Status:** | | | | +|:-- |:-- |:-- |:-- | +| [![Release](https://github.com/Exiv2/exiv2/actions/workflows/release.yml/badge.svg)](https://github.com/Exiv2/exiv2/actions/workflows/release.yml) | [![Basic jobs for all platforms](https://github.com/Exiv2/exiv2/actions/workflows/on_push_BasicWinLinMac.yml/badge.svg?branch=main)](https://github.com/Exiv2/exiv2/actions/workflows/on_push_BasicWinLinMac.yml) | [![Nightly jobs for Linux distributions](https://github.com/Exiv2/exiv2/actions/workflows/nightly_Linux_distributions.yml/badge.svg?branch=main)](https://github.com/Exiv2/exiv2/actions/workflows/nightly_Linux_distributions.yml) | [![On PUSH - Linux Special Builds for main branch](https://github.com/Exiv2/exiv2/actions/workflows/on_push_ExtraJobsForMain.yml/badge.svg)](https://github.com/Exiv2/exiv2/actions/workflows/on_push_ExtraJobsForMain.yml) | + +
-
# Welcome to Exiv2 +![Exiv2](exiv2.png) + Exiv2 is a C++ library and a command-line utility to read, write, delete and modify Exif, IPTC, XMP and ICC image metadata. @@ -16,70 +25,74 @@ write, delete and modify Exif, IPTC, XMP and ICC image metadata. The file ReadMe.txt in a build bundle describes how to install the library on the platform. ReadMe.txt also documents how to compile and link code on the platform.
-### TABLE OF CONTENTS -![Exiv2](exiv2.png) -1. [Welcome to Exiv2](#1) -2. [Building, Installing, Using and Uninstalling Exiv2](#2) - 1. [Build, Install, Use and Uninstall Exiv2 on a UNIX-like system](#2-1) - 2. [Build and Install Exiv2 with Visual Studio](#2-2) - 3. [Build Options](#2-3) - 4. [Dependencies](#2-4) - 5. [Building and linking your code with Exiv2](#2-5) - 6. [Consuming Exiv2 with CMake](#2-6) - 7. [Using pkg-config to compile and link your code with Exiv2](#2-7) - 8. [Localisation](#2-8) - 9. [Building Exiv2 Documentation](#2-9) - 10. [Building Exiv2 Packages](#2-10) - 11. [Debugging Exiv2](#2-11) - 12. [Building Exiv2 with Clang and other build chains](#2-12) - 13. [Building Exiv2 with ccache](#2-13) - 14. [Thread Safety](#2-14) - 15. [Library Initialisation and Cleanup](#2-15) - 16. [Cross Platform Build and Test on Linux for MinGW](#2-16) - 17. [Building with C++11 and other compilers](#2-17) - 18. [Static and Shared Libraries](#2-18) - 19. [Support for bmff files (CR3, HEIF, HEIC, and AVIF)](#2-19) -3. [License and Support](#3) - 1. [License](#3-1) - 2. [Support](#3-2) -4. [Test Suit](#4) - 1. [Running tests on a UNIX-like system](#4-1) - 2. [Running tests on Visual Studio builds](#4-2) - 3. [Unit tests](#4-3) - 4. [Python tests](#4-4) - 5. [Test Summary](#4-5) -5. [Platform Notes](#5) - 1. [Linux](#5-1) - 2. [macOS](#5-2) - 3. [MinGW/msys2](#5-3) - 4. [Cygwin](#5-4) - 5. [Visual Studio](#5-5) - 6. [Unix](#5-6) +# TABLE OF CONTENTS + +- [Welcome to Exiv2](#Welcome) +- [Building, Installing, Using and Uninstalling Exiv2](#B_I_U) + - [Build, Install, Use and Uninstall Exiv2 on a UNIX-like system](#B_I_U_Unix) + - [Build and Install Exiv2 with Visual Studio](#B_I_U_VisualStudio) + - [Configure the project with CMake presets](#CMakePresets) + - [Build Options](#BuildOptions) + - [Dependencies](#Dependencies) + - [Building and linking your code with Exiv2](#BuildAndLinkYourCode) + - [Consuming Exiv2 with CMake](#ConsumeExiv2WithCmake) + - [Using pkg-config to compile and link your code with Exiv2](#ConsumeWithPkgConfig) + - [Localisation](#Localisation) + - [Building Exiv2 Documentation](#BuildDoc) + - [Building Exiv2 Tag Webpages](#BuildTagWebpages) + - [Building Exiv2 Packages](#GeneratePackages) + - [Debugging Exiv2](#Debugging) + - [Building Exiv2 with Clang and other build chains](#BuildWithClangAndOthers) + - [Building Exiv2 with ccache](#CCache) + - [Thread Safety](#ThreadSafety) + - [Library Initialisation and Cleanup](#InitAndCleanup) + - [Cross Platform Build and Test on Linux for MinGW](#CrossPlatformSupport) + - [Static and Shared Libraries](#StaticShared) + - [Support for BMFF files (e.g., CR3, HEIF, HEIC, AVIF, and JPEG XL)](#BMFF) +- [License and Support](#LicenseSupport) + - [License](#License) + - [Support](#Support) +- [Test Suite](#TestSuite) + - [Exiv2 Environment Variables](#EnvironmentVariables) + - [Running tests on a UNIX-like system](#TestsOnUnix) + - [Running tests on Visual Studio builds](#TestsOnVisualStudio) + - [Unit Tests](#UnitTests) + - [Bugfix Tests](#BugfixTests) + - [Fuzzing](#FuzzingTests) + - [OSS-Fuzz](#OssFuzz) +- [Platform Notes](#PlatformNotes) + - [Linux](#PlatformLinux) + - [macOS](#PlatformMacOs) + - [MinGW/msys2](#PlatformMinGWMSYS) + - [Cygwin](#PlatformCygwin) + - [Visual Studio](#PlatformVisualStudio) + - [Unix](#PlatformUnix) [TOC](#TOC) -
-## 2 Building, Installing, Using and Uninstalling Exiv2 +
+ +# Building, Installing, Using and Uninstalling Exiv2 + +You need [CMake](https://cmake.org/download/) to configure the Exiv2 project, any C++ compiler implementing the C++ 20 standard and the associated tool chain. -You need [CMake](https://cmake.org/download/) to configure the Exiv2 project and the GCC or Clang compiler and associated tool chain. +
-
-### 2.1 Build, Install, Use Exiv2 on a UNIX-like system +## Build, Install, Use Exiv2 on a UNIX-like system ```bash -$ cd ~/gnu/github/exiv2 # location of the project code -$ mkdir build && cd build -$ cmake .. -DCMAKE_BUILD_TYPE=Release -$ cmake --build . -$ make tests -$ sudo make install +$ cd ~/gnu/github/exiv2 # Location of the project code +$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release # Configure the project with CMake +$ cmake --build build # Compile the project +$ ctest --test-dir build --verbose # Run tests +$ sudo cmake --install build # Run the install target (install library, public headers, application and CMake files) ``` -This will install the library into the "standard locations". The library will be installed in `/usr/local/lib`, executables (including the exiv2 command-line program) in `/usr/local/bin/` and header files in `/usr/local/include/exiv2` +This will install the library into the "standard locations". The library will be installed in `/usr/local/lib`, executables (including the exiv2 command-line program) in `/usr/local/bin/` and header files in `/usr/local/include/exiv2`. The target directory for the installation can be modified by using the CMake option `-DCMAKE_INSTALL_PREFIX`. -cmake generates files in the build directory. cmake generates the project/solution/makefiles required to build the exiv2 library and sample applications. cmake also creates the files exv\_conf.h and exiv2lib\_export which contain compiler directives about the build options you have chosen and the availability of libraries on your machine. +CMake analyzes the project configuration from the source code directory and generates files into the build directory. It generates the project/solution/makefiles required to build the exiv2 library and command line application (and optionally sample applications and test runners). CMake also creates the files `exv_conf.h` and `exiv2lib_export.h` which contain compiler directives about the build options you have chosen and the availability of libraries on your machine. -#### Using the exiv2 command-line program +### Using the exiv2 command-line program To execute the exiv2 command line program, you should update your path to search /usr/local/bin/ @@ -94,102 +107,199 @@ $ export LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH" # Linux, Cygwin, $ export DYLD_LIBRARY_PATH="/usr/local/lib:$DYLD_LIBRARY_PATH" # macOS ``` -#### Uninstall +### Uninstall I don't know why anybody would uninstall Exiv2. ```bash $ cd ~/gnu/github/exiv2 # location of the project code -$ cd build -$ sudo make uninstall +$ cmake --build build --target uninstall ``` -These commands will remove the exiv2 executables, library, header files and man page from the standard locations. +These commands will run the `uninstall` target and remove all the files which were installed by the `install` target. +Note that this mechanism is not perfect and it is not able to remove the sub-directories created in the installation +path. [TOC](#TOC) -
-### 2.2 Build and Install Exiv2 with Visual Studio +
+ +## Build and Install Exiv2 with Visual Studio -We recommend that you use conan to download the Exiv2 external dependencies on Windows. On other platforms (maxOS, Ubuntu and others), you should use the platform package manger. These are discussed: [Platform Notes](#5) The options to configure and compile the project using Visual Studio are similar to UNIX like systems. +We recommend to use conan to download the Exiv2 external dependencies on Windows. On other platforms (macOS, Linux and others), traditionally the platform package managers have been used. However, conan can be used in any platform/architecture to bring the project dependencies. These are discussed at [Platform Notes](#PlatformNotes). The options to configure and compile the project using Visual Studio are similar to UNIX like systems. See [README-CONAN](README-CONAN.md) for more information about Conan. When you build, you may install with the following command. ```cmd -> cmake --build . --target install +> cmake --install build ``` -This will create and copy the exiv2 build artefacts to C:\Program Files (x86)\exiv2\. You should modify your path to include C:\Program Files (x86)\exiv2\bin. +This will create and copy the exiv2 build artefacts to `%ProgramFiles%/exiv2`. To be able to run the `exiv2` command line application from any terminal you should modify your path to include `%ProgramFiles%/exiv2/bin`. [TOC](#TOC) -
-### 2.3 Build options +
+ +## Configure the project with the CMake presets -There are two groups of CMake options. There are many options defined by CMake. Here are some particularly useful options: +CMake presets (see documentation [here](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html)) were added recently to the project to ease the CMake configuration process for typical configurations. The presets are defined in the file `CMakePresets.json` and they can be used from the terminal or interpreted by different IDEs. Please note that one needs to use a recent version of CMake (>= 3.21) supporting the presets feature. + +One can list the available presets using the `--list-presets` option: + +```bash +# Running the command from a Windows terminal +$ cmake --list-presets +Available configure presets: + + "msvc" - Visual Studio cl toolchain (also usable from VS Code) + "win-debug" - Windows Debug with configured architecture + "win-release" - Windows Release with configured architecture + +# Running the command from a Linux terminal +$ cmake --list-presets +Available configure presets: + + "linux-debug" - Linux Debug with default architecture + "linux-release" - Linux Release with default architecture +``` + +The project configuration with a specific preset can be chosen with the CMake `--preset` option. In the following terminal output we comment out some interesting things happening during the project configuration: + +```bash +# Configuring the project using a preset +$ cmake --preset win-release +Preset CMake variables: + +# Note that with the usage of a preset, we pass many different options to CMake. + BUILD_SHARED_LIBS:BOOL="TRUE" + CMAKE_BUILD_TYPE="Release" + CMAKE_INSTALL_PREFIX:PATH="C:/dev/personal/exiv2/build-win-release/install" + # A build & install directory are configured with the preset + CONAN_AUTO_INSTALL:BOOL="TRUE" + EXIV2_BUILD_SAMPLES:BOOL="TRUE" + EXIV2_BUILD_UNIT_TESTS:BOOL="TRUE" + EXIV2_ENABLE_BMFF:BOOL="TRUE" + EXIV2_ENABLE_CURL:BOOL="TRUE" + EXIV2_ENABLE_NLS:BOOL="FALSE" + EXIV2_ENABLE_VIDEO:BOOL="TRUE" + EXIV2_ENABLE_PNG:BOOL="TRUE" + EXIV2_ENABLE_WEBREADY:BOOL="TRUE" + EXIV2_TEAM_WARNINGS_AS_ERRORS:BOOL="TRUE" + +# Conan can be automatically detected in your system and it is run automatically to bring the +# project dependencies +-- Conan: Detected VS runtime: MD +-- Conan: checking conan executable +-- Conan: Found program C:/dev/envs/conan/Scripts/conan.exe +-- Conan: Version found Conan version 1.47.0 +-- Conan executing: C:/dev/envs/conan/Scripts/conan.exe install .. --remote conancenter --build missing --options webready=True --settings arch=x86_64 --settings build_type=Release --settings compiler=Visual Studio --settings compiler.version=20 --settings compiler.runtime=MD +... + +# CMake finds the project dependencies which were automatically handled by conan +-- Conan: Using autogenerated FindZLIB.cmake +-- Library zlib found C:/Users/luis/.conan/data/zlib/1.2.11/_/_/package/ +-- Conan: Using autogenerated FindCURL.cmake +-- Library libcurl_imp found C:/Users/luis/.conan/data/libcurl/7.79.0/_/_/package/ +... + +# CMake finish the project configuration and prints a report +-- Install prefix: C:/dev/personal/exiv2/build-win-release/install +-- ------------------------------------------------------------------ +-- CMake Generator: Ninja +-- CMAKE_BUILD_TYPE: Release +-- Compiler info: MSVC (C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.30.30705/bin/Hostx64/x64/cl.exe) ; version: 19.30.30705.0 +-- CMAKE_CXX_STANDARD:20 +-- --- Compiler flags --- +-- General: /DWIN32 /D_WINDOWS /W3 /GR /EHsc + /MP + /utf-8 + /WX +-- Extra: +-- Debug: /MDd /Zi /Ob0 /Ox /Zo +-- Release: /MD /O2 /DNDEBUG +-- RelWithDebInfo: /MD /Zi /O2 /DNDEBUG +-- MinSizeRel: /MD /O1 /DNDEBUG +-- --- Linker flags --- +-- General: /machine:x64 /WX +-- Debug: /debug /INCREMENTAL +-- Release: /INCREMENTAL:NO +-- RelWithDebInfo: /debug /INCREMENTAL +-- MinSizeRel: /INCREMENTAL:NO +-- +... +-- Build files have been written to: C:/dev/personal/exiv2/build-win-release +``` + +Note that the usage of CMake presets allow the project contributors to use the same set of options easily in different environments (using terminal, IDEs or CI). + +[TOC](#TOC) +
+ +## Build options + +There are two groups of CMake options which are relevant to the project: global CMake options and project specific ones. Here are some of the global options which are particularly useful: | Options | Purpose (_default_) | |:------------- |:------------- | -| CMAKE\_INSTALL\_PREFIX
CMAKE\_BUILD\_TYPE
BUILD\_SHARED\_LIBS | Where to install on your computer _**(/usr/local)**_
Type of build _**(Release)**_ See: [Debugging Exiv2](#2-11)
Build exiv2lib as shared or static _**(On)**_ | +| CMAKE\_INSTALL\_PREFIX
CMAKE\_BUILD\_TYPE
BUILD\_SHARED\_LIBS | Where to install on your computer _**(/usr/local)**_
Type of build _**(Release)**_ See: [Debugging Exiv2](#Debugging)
Build exiv2lib as SHARED or STATIC | -Options defined by /CMakeLists.txt include: +Options defined at `exiv2/CMakeLists.txt` include: ```bash 576 rmills@rmillsmm:~/gnu/github/exiv2/exiv2 $ grep ^option CMakeLists.txt -option( BUILD_SHARED_LIBS "Build exiv2lib as a shared library" ON ) -option( EXIV2_ENABLE_XMP "Build with XMP metadata support" ON ) -option( EXIV2_ENABLE_EXTERNAL_XMP "Use external version of XMP" OFF ) -option( EXIV2_ENABLE_PNG "Build with png support (requires libz)" ON ) +option( BUILD_SHARED_LIBS "Build exiv2lib as a shared library" ON ) +option( EXIV2_ENABLE_XMP "Build with XMP metadata support" ON ) +option( EXIV2_ENABLE_EXTERNAL_XMP "Use external version of XMP" OFF ) +option( EXIV2_ENABLE_PNG "Build with png support (requires libz)" ON ) ... -option( EXIV2_ENABLE_BMFF "Build with BMFF support" OFF ) +option( EXIV2_ENABLE_BMFF "Build with BMFF support (brotli recommended)" ON ) +option( EXIV2_ENABLE_BROTLI "Use Brotli for JPEG XL compressed boxes (BMFF)" ON ) +option( EXIV2_ENABLE_FILESYSTEM_ACCESS "Build with filesystem access" ON ) 577 rmills@rmillsmm:~/gnu/github/exiv2/exiv2 $ ``` -Options are defined on the CMake command-line: +Using the command-line, these variables can be set/updated using the option `-D`: ```bash -$ cmake -DBUILD_SHARED_LIBS=On -DEXIV2_ENABLE_NLS=Off +$ cmake -DBUILD_SHARED_LIBS=ON -DEXIV2_ENABLE_NLS=OFF ``` [TOC](#TOC) -
-### 2.4 Dependencies +
+ +## Dependencies The following Exiv2 features require external libraries: -| Feature | Package | Default | To change default | Availability | -|:-------------------------- |:-------- |:--------:| :---------------------------- |:----------- | -| PNG image support | zlib | ON | -DEXIV2\_ENABLE\_PNG=Off | [http://zlib.net/](http://zlib.net/) | -| XMP support | expat | ON | -DEXIV2\_ENABLE\_XMP=Off | [http://expat.sourceforge.net](http://expat.sourceforge.net)/
Use _**Expat 2.2.6**_ and later | -| Natural language system | gettext | OFF | -DEXIV2\_ENABLE\_NLS=On | [http://www.gnu.org/software/gettext/](http://www.gnu.org/software/gettext/) | -| Character set conversion | libiconv | | Disabled for Visual Studio.
Linked when installed on UNIX like platforms. | [https://www.gnu.org/software/libiconv/](https://www.gnu.org/software/libiconv/) | +| Feature | Package | Default | To change default | Availability | +|:------------------------ |:-------- |:-------:|:--------------------------- |:------------ | +| PNG image support | zlib | ON | -DEXIV2\_ENABLE\_PNG=OFF | [https://zlib.net/](https://zlib.net/) | +| XMP support | expat | ON | -DEXIV2\_ENABLE\_XMP=OFF | [https://libexpat.github.io/](https://libexpat.github.io/)
Use _**Expat 2.2.6**_ and later | +| Natural language system | gettext | OFF | -DEXIV2\_ENABLE\_NLS=ON | [https://www.gnu.org/software/gettext/](https://www.gnu.org/software/gettext/) | +| JPEG XL brob support | brotli | ON | -DEXIV2\_ENABLE\_BROTLI=OFF | [https://github.com/google/brotli](https://github.com/google/brotli) | +| Character set conversion | libiconv | | Disabled for Visual Studio.
Linked when installed on UNIX like platforms. | [https://www.gnu.org/software/libiconv/](https://www.gnu.org/software/libiconv/) | On UNIX systems, you may install the dependencies using the distribution's package management system. Install the development package of a dependency to install the header files and libraries required to build Exiv2. The script -`ci/install_dependencies.sh` is used to setup CI images on which we build and test Exiv2 on many platforms when we modify code. You may find that helpful in setting up your platform dependencies. +`ci/install_dependencies.sh` is used to setup the CI images on which we build and test Exiv2. You may find that helpful in setting up your platform dependencies. -Natural language system is discussed in more detail here: [Localisation](#2-8) +Natural language system is discussed in more detail here: [Localisation](#Localisation) -Notes about different platforms are included here: [Platform Notes](#5) +Notes about different platforms are included here: [Platform Notes](#PlatformNotes) You may choose to install dependences with conan. This is supported on all platforms and is especially useful for users of Visual Studio. See [README-CONAN](README-CONAN.md) for more information. ### Libiconv -The library libiconv is used to perform character set encoding in the tags Exif.Photo.UserComment, Exif.GPSInfo.GPSProcessingMethod and Exif.GPSInfo.GPSAreaInformation. This is documented in the exiv2 man page. +The library libiconv is used to perform character set encoding in the tags Exif.Photo.UserComment, Exif.GPSInfo.GPSProcessingMethod and Exif.GPSInfo.GPSAreaInformation. This is documented in the [exiv2 man page](exiv2.md). CMake will detect libiconv of all UNIX like systems including Linux, macOS, UNIX, Cygwin64 and MinGW/msys2. If you have installed libiconv on your machine, Exiv2 will link and use it. The library libiconv is a GNU library and we do not recommend using libiconv with Exiv2 when building with Visual Studio. -Exiv2 includes the file cmake/FindIconv.cmake which contains a guard to prevent CMake from finding libiconv when you build with Visual Studio. This was added because of issues reported when Visual Studio attempted to link libconv libraries installed by Cygwin, or MinGW or gnuwin32. [https://github.com/Exiv2/exiv2/issues/1250](https://github.com/Exiv2/exiv2/issues/1250) - -There are build instructions about Visual Studio in libiconv-1.16/INSTALL.window require you to install Cygwin. There is an article here about building libiconv with Visual Studio. [https://www.codeproject.com/Articles/302012/How-to-Build-libiconv-with-Microsoft-Visual-Studio](https://www.codeproject.com/Articles/302012/How-to-Build-libiconv-with-Microsoft-Visual-Studio). - -If you wish to use libiconv with Visual Studio you will have to build libiconv and remove the "guard" in cmake/FindIconv.cmake. Team Exiv2 will not provide support concerning libiconv and Visual Studio. - [TOC](#TOC) -
-### 2.5 Building and linking your code with Exiv2 +
+ +## Building and linking your code with Exiv2 There are detailed platform notes about compiling and linking in `releasenotes/{platform}/ReadMe.txt` @@ -197,7 +307,7 @@ where `platform: { CYGWIN | Darwin | Linux | MinGW | msvc | Unix }` In general you need to do the following: -1) Application code should be written in C++98 and include exiv2 headers: +1) Application code should be written in at least C++14 and include exiv2 headers: ```cpp #include @@ -210,14 +320,15 @@ In general you need to do the following: The following is a typical command to build and link with libexiv2: ```bash -$ g++ -std=c++98 myprog.cpp -o myprog -I/usr/local/include -L/usr/local/lib -lexiv2 +$ g++ -std=c++20 myprog.cpp -o myprog -I/usr/local/include -L/usr/local/lib -lexiv2 ``` [TOC](#TOC) -
-### 2.6 Consuming Exiv2 with CMake +
+ +## Consuming Exiv2 with CMake -When exiv2 is installed, the files required to consume Exiv2 are installed in `${CMAKE_INSTALL_PREFIX}/lib/cmake/exiv2` +When exiv2 is installed, the files required to consume Exiv2 with CMake are installed in `${CMAKE_INSTALL_PREFIX}/lib/cmake/exiv2` You can build samples/exifprint.cpp as follows: @@ -226,26 +337,27 @@ $ cd $ mkdir exifprint $ cd exifprint $ cat - > CMakeLists.txt < -### 2.7 Using pkg-config to compile and link your code with Exiv2 +
+ +## Using pkg-config to compile and link your code with Exiv2 When exiv2 is installed, the file exiv2.pc used by pkg-config is installed in `${CMAKE_INSTALL_PREFIX}/lib/pkgconfig` You will need to set the following in your environment: @@ -264,18 +376,19 @@ LDFLAGS := `pkg-config exiv2 --libs` If you are not using make, you can use pkg-config as follows: ```bash -g++ -std=c++98 myprogram.cpp -o myprogram $(pkg-config exiv2 --libs --cflags) +g++ -std=c++20 myprogram.cpp -o myprogram $(pkg-config exiv2 --libs --cflags) ``` [TOC](#TOC) -
-### 2.8 Localisation +
+ +## Localisation Localisation is supported on a UNIX-like platform: Linux, macOS, Cygwin and MinGW/msys2. Localisation is not supported for Visual Studio builds. Crowdin have provided Exiv2 with a free open-source license to use their services. The Exiv2 localisation project is located at [https://crowdin.com/project/exiv2](https://crowdin.com/project/exiv2). You will also need to register to have a free user account on Crowdin. The Crowdin setup is discussed here: [https://github.com/Exiv2/exiv2/issues/1510](https://github.com/Exiv2/exiv2/issues/1510). It is recommended that you coordinate with Leonardo before contributing localisation changes on Crowdin. You can contact Leonardo by via GitHub. -To build localisation support, use the CMake option `-DEXIV2_ENABLE_NLS=On`. You must install the `gettext` package with your package manager or from source. The `gettext` package is available from [http://www.gnu.org/software/gettext/](http://www.gnu.org/software/gettext/) and includes the library `libintl` and utilities to build localisation files. If CMake produces error messages which mention libintl or gettext, you should verify that the package `gettext` has been correctly built and installed. +To build localisation support, use the CMake option `-DEXIV2_ENABLE_NLS=ON`. You must install the `gettext` package with your package manager or from source. The `gettext` package is available from [https://www.gnu.org/software/gettext/](https://www.gnu.org/software/gettext/) and includes the library `libintl` and utilities to build localisation files. If CMake produces error messages which mention libintl or gettext, you should verify that the package `gettext` has been correctly built and installed. You must install the build to test localisation. This ensures that the localisation message files can be found at run-time. You cannot test localisation in the directory `build\bin`. @@ -285,9 +398,9 @@ You must install the build to test localisation. This ensures that the localisa $ env LANG=fr_FR exiv2 # env LANGUAGE=fr_FR exiv2 on Linux! exiv2: Une action doit ÃĒtre spÊcifiÊ exiv2: Au moins un fichier est nÊcessaire -Utilisation : exiv2 [ options ] [ action ] fichier ... +Utilisation : exiv2 [ option [ arg ] ]+ [ action ] fichier ... -Manipulation des mÊtadonnÊes EXIF issues des images. +Image metadata manipulation tool. $ ``` @@ -308,14 +421,14 @@ I edited the following: ```bash #: src/exiv2.cpp:237 -msgid "Manipulate the Exif metadata of images.\n" +msgid "Image metadata manipulation tool.\n" msgstr "" ``` to: ```bash #: src/exiv2.cpp:237 -msgid "Manipulate the Exif metadata of images.\n" +msgid "Image metadata manipulation tool.\n" msgstr "Manipulate image metadata.\n" ``` @@ -336,7 +449,7 @@ $ sudo cp -R po/xy/LC_MESSAGES/exiv2.mo /usr/local/share/locale/xy/LC_MESSAGES $ env LANG=xy exiv2 # env LANGUAGE=xy on Linux! exiv2: An action must be specified exiv2: At least one file is required -Usage: exiv2 [ options ] [ action ] file ... +Usage: exiv2 [ option [ arg ] ]+ [ action ] file ... Manipulate image metadata. <--------- Edited message! $ @@ -357,35 +470,69 @@ $ ``` [TOC](#TOC) -
-### 2.9 Building Exiv2 Documentation +
+ +## Building Exiv2 Documentation Building documentation requires installing special tools. You will probably prefer to read the documentation on-line from the project website: https://exiv2.org -To build documentation, use the CMake option **`-DEXIV2_BUILD_DOC=On`**. +To build documentation, use the CMake option **`-DEXIV2_BUILD_DOC=ON`**. Additionally, you will require an additional build step to actually build the documentation. ```bash -$ cmake ..options.. -DEXIV2_BUILD_DOC=On -$ make doc +$ cmake ..options.. -DEXIV2_BUILD_DOC=ON +$ cmake --build build --target doc ``` To build the documentation, you must install the following products: | Product | Availability | |:------------ |:------------ | -| doxygen
graphviz
python
xsltproc
md5sum | [http://www.doxygen.org/](http://www.doxygen.org/)
[http://www.graphviz.org/](http://www.graphviz.org/)
[http://www.python.org/](http://www.python.org/)
[http://xmlsoft.org/XSLT/](http://xmlsoft.org/XSLT/)
[http://www.microbrew.org/tools/md5sha1sum/](http://www.microbrew.org/tools/md5sha1sum/) | +| doxygen
graphviz
python
xsltproc
md5sum | [https://www.doxygen.nl/](https://www.doxygen.nl/)
[https://www.graphviz.org/](https://www.graphviz.org/)
[https://www.python.org/](https://www.python.org/)
[http://xmlsoft.org/XSLT/](http://xmlsoft.org/XSLT/)
[http://www.microbrew.org/tools/md5sha1sum/](http://www.microbrew.org/tools/md5sha1sum/) | + +[TOC](#TOC) +
+ +## Building Exiv2 Tag Webpages + +Exiv2 provides many built-in metadata tags which are listed in the sub-pages of https://exiv2.org/metadata.html +and https://pre-release.exiv2.org/metadata.html. Those tag webpages are generated using tag information +extracted from the Exiv2 source code. + +The tag webpage build files are in the `/doc/templates` directory. If changes are made to +tag groups in the Exiv2 source code then the build files need to be updated. Any changes made +to individual tags in an existing tag group are automatically included. + +Building the tag webpages requires building the Exiv2 sample programs and using scripts which have additional dependencies on +[BASH](https://www.gnu.org/software/bash/), [make](https://manpages.org/make), [xsltproc](https://manpages.org/xsltproc) +and [Python3](https://www.python.org/). + +To build the tag webpages, first [build Exiv2 from source](#TOC) with the `-DEXIV2_BUILD_SAMPLES=ON` +option enabled. This is required as the [taglist](README-SAMPLES.md#taglist) sample program is used by one of the scripts. + +Next, set the `EXIV2_BINDIR` environment variable (see [Exiv2 environment variables](#EnvironmentVariables)). + +Then, change directory to `doc/templates` and run `make`. + +```bash +$ cd /doc/templates +$ make +``` + +After processing, the generated webpages are stored in the `/doc/templates` directory. +When the Exiv2 websites are updated, the generated tag webpages are reformatted before use. [TOC](#TOC) -
-### 2.10 Building Exiv2 Packages +
+ +## Building Exiv2 Packages -To enable the building of Exiv2 packages, use the CMake option `-DEXIV2_TEAM_PACKAGING=On`. +To enable the building of Exiv2 packages, use the CMake option `-DEXIV2_TEAM_PACKAGING=ON`. You should not build Exiv2 Packages. This feature is intended for use by Team Exiv2 to create Platform and Source Packages on the buildserver. -There are two types of Exiv2 packages which are generated by cpack from the cmake command-line. +There are two types of Exiv2 packages which are generated by cpack from the CMake command-line. 1) Platform Package (header files, binary library and samples. Some documentation and release notes) @@ -393,15 +540,14 @@ Create and build exiv2 for your platform. ```bash $ git clone https://github.com/exiv2/exiv2 -$ mkdir -p exiv2/build -$ cd exiv2/build -$ cmake .. -G "Unix Makefiles" -DEXIV2_TEAM_PACKAGING=On +$ cd exiv2 +$ cmake -S . -B build -G "Unix Makefiles" -DEXIV2_TEAM_PACKAGING=ON ... -- Build files have been written to: .../build -$ cmake --build . --config Release +$ cmake --build build --config Release ... [100%] Built target addmoddel -$ make package +$ cmake --build build --target package ... CPack: - package: /path/to/exiv2/build/exiv2-0.27.1-Linux.tar.gz generated. ``` @@ -409,29 +555,25 @@ CPack: - package: /path/to/exiv2/build/exiv2-0.27.1-Linux.tar.gz generated. 2) Source Package ```bash -$ make package_source +$ cmake --build build --target package_source Run CPack packaging tool for source... ... CPack: - package: /path/to/exiv2/build/exiv2-0.27.1-Source.tar.gz generated. ``` -You may prefer to run `$ cmake --build . --config Release --target package_source` - - [TOC](#TOC) -
-### 2.11 Debugging Exiv2 +
+ +## Debugging Exiv2 1) Generating and installing a debug library -In general to generate a debug library, you should use the option *cmake* option `-DCMAKE_RELEASE_TYPE=Debug` and build in the usual way. +In general to generate a debug library, you should use the *CMake* option `-DCMAKE_RELEASE_TYPE=Debug` and build in the usual way. ```bash $ cd -$ mkdir build -$ cd build -$ cmake .. -G "Unix Makefiles" "-DCMAKE_BUILD_TYPE=Debug" -$ make +$ cmake -S . -B build -G "Unix Makefiles" "-DCMAKE_BUILD_TYPE=Debug" +$ cmake --build build ``` @@ -445,9 +587,6 @@ exiv2 0.27.1 debug=1 $ ``` - -[TOC](#TOC) - 2) About preprocessor symbols `NDEBUG` and `EXIV2_DEBUG_MESSAGES` Exiv2 respects the symbol `NDEBUG` which is set only for Release builds. There are sequences of code which are defined within: @@ -462,18 +601,16 @@ Those blocks of code are not compiled unless you define `EXIV2_DEBUG_MESSAGES`. ```bash $ cd -$ touch src/webpimage.cpp -$ make CXXFLAGS=-DEXIV2_DEBUG_MESSAGES +$ cmake -S . -B build -DCMAKE_CXX_FLAGS=-DEXIV2_DEBUG_MESSAGES +$ cmake --build build $ bin/exiv2 ... -- or -- -$ sudo make install +$ cmake --install build $ exiv2 ... ``` If you are debugging library code, it is recommended that you use the exiv2 command-line program as your test harness as Team Exiv2 is very familiar with this tool and able to give support. -[TOC](#TOC) - 3) Starting the debugger This is platform specific. On Linux: @@ -482,8 +619,6 @@ This is platform specific. On Linux: $ gdb exiv2 ``` -[TOC](#TOC) - 4) Using Debugger IDEs such as Xcode, CLion, Visual Studio, Eclipse or QtCreator I have used all those IDEs to debug the Exiv2 library and applications. All of them work. You may find it takes initial effort, however I assure you that they all work well. @@ -491,19 +626,17 @@ I have used all those IDEs to debug the Exiv2 library and applications. All of I work on macOS and use Xcode to develop Exiv2. For a couple of years, Team Exiv2 had free open-source licences from JetBrains for CLion. I really liked CLion as it is cross platform and runs on Windows, Mac and Linux. It has excellent integration with CMake and will automatically -add **`-DCMAKE_BUILD_TYPE=Debug`** to the cmake command. It keeps build types in separate directories +add **`-DCMAKE_BUILD_TYPE=Debug`** to the CMake command. It keeps build types in separate directories such as **`/cmake-build-debug`**. -[TOC](#TOC) - -5) cmake --build . options **`--config Release|Debug`** and **`--target install`** +5) cmake --build build options **`--config Release|Debug`** and **`--target install`** -Visual Studio and Xcode can build debug or release builds without using the option **`-DCMAKE_BUILD_TYPE`** because the generated project files can build multiple types. The option **`--config Debug`** can be specified on the cmake command-line to specify the build type. Alternatively, if you prefer to build in the IDE, the UI provides options to select the configuration and target. +Visual Studio and Xcode can build debug or release builds without using the option **`-DCMAKE_BUILD_TYPE`** because the generated project files can build multiple types. The option **`--config Debug`** can be specified on the CMake command-line to specify the build type. Alternatively, if you prefer to build in the IDE, the UI provides options to select the configuration and target. With the Unix Makefile generator, the targets can be listed: ```bash -$ make help +$ cmake --build build --target help The following are some of the valid targets for this Makefile: ... all (the default if no target is provided) ... clean @@ -513,16 +646,17 @@ The following are some of the valid targets for this Makefile: ``` [TOC](#TOC) -
-### 2.12 Building Exiv2 with **clang** and other build chains +
+ +## Building Exiv2 with **clang** and other build chains 1) On Linux ```bash $ cd -$ rm -rf build ; mkdir build ; cd build -$ cmake .. -DCMAKE_C_COMPILER=$(which clang) -DCMAKE_CXX_COMPILER=$(which clang++) -$ cmake --build . +$ rm -rf build +$ cmake -S . -B build -DCMAKE_C_COMPILER=$(which clang) -DCMAKE_CXX_COMPILER=$(which clang++) +$ cmake --build build ``` **_OR_** @@ -531,9 +665,9 @@ $ cmake --build . $ export CC=$(which clang) $ export CXX=$(which clang++) $ cd -$ rm -rf build ; mkdir build ; cd build -$ cmake .. -$ cmake --build . +$ rm -rf build +$ cmake -S . -B build +$ cmake --build build ``` 2) On macOS @@ -545,8 +679,9 @@ Apple provide clang with Xcode. GCC has not been supported by Apple since 2013. I have been unable to get clang to work on any of those platforms. [TOC](#TOC) -
-### 2.13 Building Exiv2 with ccache +
+ +## Building Exiv2 with ccache To speed up compilation, the utility ccache can be installed to cache the output of the compiler. This greatly speeds up the build when you frequently built code that has not been modified. @@ -556,23 +691,23 @@ Installing and using ccache (and other similar utilities), is platform dependent $ sudo apt install --yes ccache ``` -To build with ccache, use the cmake option **-DBUILD\_WITH\_CCACHE=On** +To build with ccache, use the CMake option **-DBUILD\_WITH\_CCACHE=ON** ```bash $ cd -$ mkdir build ; cd build ; cd build -$ cmake .. -G "Unix Makefiles" -DBUILD_WITH_CCACHE=On -$ make +$ cmake -S . -B build -G "Unix Makefiles" -DBUILD_WITH_CCACHE=ON +$ cmake --build build # Build again to appreciate the performance gain -$ make clean -$ make +$ cmake --build build --target clean +$ cmake --build build ``` -Due to the way in which ccache is installed in Fedora (and other Linux distros), ccache effectively replaces the compiler. A default build or **-DBUILD\_WITH\_CCACHE=Off** is not effective and the environment variable CCACHE_DISABLE is required to disable ccache. [https://github.com/Exiv2/exiv2/issues/361](https://github.com/Exiv2/exiv2/issues/361) +Due to the way in which ccache is installed in Fedora (and other Linux distros), ccache effectively replaces the compiler. A default build or **-DBUILD\_WITH\_CCACHE=OFF** is not effective and the environment variable CCACHE_DISABLE is required to disable ccache. [https://github.com/Exiv2/exiv2/issues/361](https://github.com/Exiv2/exiv2/issues/361) [TOC](#TOC) -
-### 2.14 Thread Safety +
+ +## Thread Safety Exiv2 heavily relies on standard C++ containers. Static or global variables are used read-only, with the exception of the XMP namespace registration function (see below). Thus Exiv2 is thread safe in the same sense as C++ containers: Different instances of the same class can safely be used concurrently in multiple threads. @@ -589,17 +724,14 @@ int main(int argc, const char* argv[]) { Exiv2::XmpParser::initialize(); ::atexit(Exiv2::XmpParser::terminate); -#ifdef EXV_ENABLE_BMFF - Exiv2::enableBMFF(true); -#endif ... } ``` -The use of the _**thread unsafe function**_ Exiv2::enableBMFF(true) is discussed in [2.19 Support for bmff files](#2-19) [TOC](#TOC) -
-### 2.15 Library Initialisation and Cleanup +
+ +## Library Initialisation and Cleanup As discussed in the section on Thread Safety, Exiv2 classes for Exif and IPTC metadata are fully reentrant and require no initialisation or cleanup. @@ -610,26 +742,24 @@ The exiv2 command-line program and sample applications call the following at the ```cpp Exiv2::XmpParser::initialize(); ::atexit(Exiv2::XmpParser::terminate); -#ifdef EXV_ENABLE_BMFF - Exiv2::enableBMFF(true); -#endif ``` [TOC](#TOC) -
-### 2.16 Cross Platform Build and Test on Linux for MinGW +
+ +## Cross Platform Build and Test on Linux for MinGW You can cross compile Exiv2 on Linux for MinGW. We have used the following method on **Fedora** and believe this is also possible on Ubuntu and other distros. Detailed instructions are provided here for **Fedora**. ### Cross Build and Test On Fedora -####1 Install the cross platform build tools +#### 1 Install the cross platform build tools ```bash $ sudo dnf install mingw64-gcc-c++ mingw64-filesystem mingw64-expat mingw64-zlib cmake make ``` -####2 Install Dependancies +#### 2 Install Dependencies You will need to install x86_64 libraries to support the options you wish to use. By default, you will need libz and expat. Your `dnf` command above has installed them for you. If you wish to use features such as `webready` you should install openssl and libcurl as follows: @@ -644,7 +774,7 @@ Installing: ... ``` -####3 Get the code and build +#### 3 Get the code and build ```bash $ git clone://github.com/exiv2/exiv2 --branch 0.27-maintenance exiv2 @@ -657,16 +787,14 @@ $ make Note, you may wish to choose to build with optional features and/or build static libraries. To do this, request appropriately on the mingw64-cmake command: ```bash -$ mingw64-cmake .. -DEXIV2_TEAM_EXTRA_WARNINGS=On \ - -DEXIV2_ENABLE_VIDEO=On \ - -DEXIV2_ENABLE_WEBREADY=On \ - -DEXIV2_ENABLE_WIN_UNICODE=On \ - -DBUILD_SHARED_LIBS=Off +$ mingw64-cmake .. -DEXIV2_TEAM_EXTRA_WARNINGS=ON \ + -DEXIV2_ENABLE_WEBREADY=ON \ + -DBUILD_SHARED_LIBS=OFF ``` -The options available for cross-compiling are the same as provided for all builds. See: [Build Options](#2-3) +The options available for cross-compiling are the same as provided for all builds. See: [Build Options](#BuildOptions) -####4 Copy "system dlls" in the bin directory +#### 4 Copy "system dlls" in the bin directory These DLLs are required to execute the cross-platform build in the bin from Windows @@ -676,7 +804,7 @@ for i in libexpat-1.dll libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll z done ``` -####5 Executing exiv2 in wine +#### 5 Executing exiv2 in wine You may wish to use wine to execute exiv2 from the command prompt. To do this: @@ -684,19 +812,17 @@ You may wish to use wine to execute exiv2 from the command prompt. To do this: [rmills@rmillsmm-fedora build_mingw_fedora]$ wine cmd Microsoft Windows 6.1.7601 -Z:\Home\gnu\github\exiv2\0.27-maintenance\build_mingw_fedora>bin\exiv2 +Z:\Home\gnu\github\exiv2\main\build_mingw_fedora>bin\exiv2 exiv2: An action must be specified exiv2: At least one file is required -Usage: exiv2 [ options ] [ action ] file ... - -Manipulate the Exif metadata of images. +Usage: exiv2 [ option [ arg ] ]+ [ action ] file ... -Z:\Home\gnu\github\exiv2\0.27-maintenance\build_mingw_fedora> +Image metadata manipulation tool. ``` If you have not installed wine, Fedora will offer to install it for you. -####6 Running the test suite +#### 6 Running the test suite On a default wine installation, you are in the MSDOS/cmd.exe prompt. You cannot execute the exiv2 test suite in this environment as you require python3 and MSYS/bash to run the suite. @@ -716,41 +842,19 @@ On MinGW/msys2, I can directly access the share: ```bash $ cd //Mac/Home/gnu/github/exiv2/0.27/maintenance/build_mingw_fedora -$ export EXIV2_BINDIR=$pwd/bin -$ cd ../test -$ make tests +$ mingw64-ctest ``` You will find that 3 tests fail at the end of the test suite. It is safe to ignore those minor exceptions. [TOC](#TOC) -
-### 2.17 Building with C++11 and other compilers - -Exiv2 uses the default compiler for your system. Exiv2 v0.27 was written to the C++ 1998 standard and uses auto\_ptr. The C++11 and C++14 compilers will issue deprecation warnings about auto\_ptr. As _auto\_ptr support has been removed from C++17, you cannot build Exiv2 v0.27 with C++17 or later compilers._ Exiv2 v0.28 and later do not use auto\_ptr and will build with all modern C++ Standard Compilers. - -To build with C++11: - -```bash -$ cd -$ mkdir build ; cd build -$ cmake .. -DCMAKE_CXX_STANDARD=11 -DCMAKE_CXX_FLAGS=-Wno-deprecated -$ make -``` - -The option -DCMAKE\_CXX\_STANDARD=11 specifies the C++ Language Standard. Possible values are 98, 11 or 14. - -The option -DCMAKE\_CXX\_FLAGS=-Wno-deprecated suppresses warnings from C++11 concerning auto\_ptr. The compiler will issue deprecation warnings about video, eps and ssh code in Exiv2 v0.27. This is intentional. These features of Exiv2 will not be available in Exiv2 v0.28. +
-**Caution:** Visual Studio users should not use -DCMAKE\_CXX\_FLAGS=-Wno-deprecated. +## Static and Shared Libraries -[TOC](#TOC) -
-### 2.18 Static and Shared Libraries - -You can build either static or shared libraries. Both can be linked with either static or shared run-time libraries. You specify the shared/static with the option `-BUILD_SHARED_LIBS=On|Off` You specify the run-time with the option `-DEXIV2_ENABLE_DYNAMIC_RUNTIME=On|Off`. The default for both options default is On. So you build shared and use the shared libraries which are `.dll` on Windows (msvc, Cygwin and MinGW/msys), `.dylib` on macOS and `.so` on Linux and UNIX. +You can build either static or shared libraries. Both can be linked with either static or shared run-time libraries. You specify the shared/static with the option `-BUILD_SHARED_LIBS=ON|OFF` You specify the run-time with the option `-DEXIV2_ENABLE_DYNAMIC_RUNTIME=ON|OFF`. The default for both options default is ON. So you build shared and use the shared libraries which are `.dll` on Windows (msvc, Cygwin and MinGW/msys), `.dylib` on macOS and `.so` on Linux and UNIX. -CMake creates your build artefacts in the directories `bin` and `lib`. The `bin` directory contains your executables and .dlls. The `lib` directory contains your static libraries. When you install exiv2, the build artefacts are copied to your system's prefix directory which by default is `/usr/local/`. If you wish to test and use your build without installing, you will have to set you PATH appropriately. Linux/Unix users should also set `LD_LIBRARY_PATH` and macOS users should set `DYLD_LIBRARY_PATH`. +CMake creates your build artefacts in the directories `bin` and `lib`. The `bin` directory contains your executables and .DLLs. The `lib` directory contains your static libraries. When you install exiv2, the build artefacts are copied to your system's prefix directory which by default is `/usr/local/`. If you wish to test and use your build without installing, you will have to set you PATH appropriately. Linux/Unix users should also set `LD_LIBRARY_PATH` and macOS users should set `DYLD_LIBRARY_PATH`. The default build is SHARED/DYNAMIC and this arrangement treats all executables and shared libraries in a uniform manner. @@ -775,32 +879,27 @@ endif() This is discussed: [https://github.com/Exiv2/exiv2/issues/1230](https://github.com/Exiv2/exiv2/issues/1230) [TOC](#TOC) -
-### 2.19 Support for bmff files (CR3, HEIF, HEIC, and AVIF) - -**Attention is drawn to the possibility that bmff support may be the subject of patent rights. _Exiv2 shall not be held responsible for identifying any or all such patent rights. Exiv2 shall not be held responsible for the legal consequences of the use of this code_.** +
-Access to the bmff code is guarded in two ways. Firstly, you have to build the library with the cmake option: `-DEXIV2_ENABLE_BMFF=On`. Secondly, the application must enable bmff support at run-time by calling the following function. - -```cpp -EXIV2API bool enableBMFF(bool enable); -``` +## Support for BMFF files (e.g., CR3, HEIF, HEIC, AVIF, and JPEG XL) -The return value from `enableBMFF()` reports the current status of bmff support before calling this function. +**Attention is drawn to the possibility that BMFF support may be the subject of patent rights. _Exiv2 shall not be held responsible for identifying any or all such patent rights. Exiv2 shall not be held responsible for the legal consequences of the use of this code_.** -Applications may wish to provide a preference setting to enable bmff support and thereby place the responsibility for the use of this code with the user of the application. +Access to the BMFF code is guarded by the CMake option: `-DEXIV2_ENABLE_BMFF=ON` (enabled by default). [TOC](#TOC) -
-## 3 License and Support +
+ +# License and Support All project resources are accessible from the project website. https://github.com/Exiv2/exiv2 -
-### 3.1 License +
+ +## License -Copyright (C) 2004-2021 Exiv2 authors. +Copyright (C) 2004-2026 Exiv2 authors. You should have received a copy of the file [COPYING](COPYING) which details the GPLv2 license. Exiv2 is free software; you can redistribute it and/or modify @@ -819,72 +918,107 @@ with this program; if not, write to the Free Software Foundation, Inc., [TOC](#TOC) -
-### 3.2 Support +
+ +## Support For new bug reports, feature requests and support: Please open an issue in Github. [https://github.com/exiv2/exiv2](https://github.com/exiv2/exiv2) [TOC](#TOC) -
-## 4 Running the test suite - -#### Different kinds of tests: - -| Description | Language | Location | Command
_(in build directory)_ | CMake Option to Build | -|:-- |:-- |:-- |:-- |:-- | -| Run all tests | | | $ make tests | | -| Run all tests | | **Visual Studio Users** | > cmake --build . --target tests | | -| Bash tests | python | tests/bash\_tests | $ make bash_tests | -DEXIV2\_BUILD\_SAMPLES=On | -| Python tests | python | tests | $ make python_tests | -DEXIV2\_BUILD\_SAMPLES=On | -| Unit tests | C++ | unitTests | $ make unit_test | -DEXIV2\_BUILD\_UNIT\_TESTS=On | -| Version test | C++ | src/version.cpp | $ make version_test | Always in library | - -The term _**bash scripts**_ is historical. The implementation of the tests in this collection originally required bash. These -scripts have been rewritten in python. Visual Studio Users will appreciate the python implementation as it avoids the -installation of mingw/cygwin and special PATH settings. - -#### Environment Variables used by the test suite: - -If you build the code in the directory \build, tests will run using the default values of Environment Variables. - -| Variable | Default | Platforms | Purpose | -|:-- |:-- |:-- |:-- | -| EXIV2_BINDIR | **\/build/bin** | All Platforms | Path of built binaries (exiv2.exe) | -| EXIV2_PORT | **12762**
**12671**
**12760** | Cygwin
MinGW/msys2
Other Platforms | Test TCP/IP Port | -| EXIV2_HTTP | **http://localhost** | All Platforms | Test http server | -| EXIV2_ECHO | _**not set**_ | All Platforms | For debugging bash scripts | -| VALGRIND | _**not set**_ | All Platforms | For debugging bash scripts | -| VERBOSE | _**not set**_ | All Platforms | Causes make to report its actions | +
+ +# Test Suite + +You execute the Test Suite using CTest with the command `$ ctest --test-dir build`. + +The build creates 6 tests: bashTests, bugfixTests, lensTests, tiffTests, unitTests and versionTests. You can run all tests or a subset. To list all available tests, execute ctest with the `-N` or `--show-only` option, which disables execution: + +```bash +.../exiv2 $ ctest --test-dir build --show-only +Test project .../exiv2/build + Test #1: bashTests + Test #2: bugfixTests + Test #3: lensTests + Test #4: tiffTests + Test #5: versionTests + Test #6: unitTests + +Total Tests: 6 +.../exiv2 $ +``` + +ctest provides many option and the following show common use-case scenarios: + +```bash +$ ctest --test-dir build # run all tests and display summary +$ ctest --test-dir build --output-on-failure # run all tests and output failures +$ ctest --test-dir build -R bugfix # run only bugfixTests and display summary +$ ctest --test-dir build -R bugfix --verbose # run only bugfixTests and display all output +``` + +Except for the `unitTests`, CMake needs to find a python3 interpreter in the system to be able to run the rest of the test targets with CTest: + +| Name | Language | Location | Command
_(in build directory)_ | CMake Option to Build | +|:-- |:-- |:-- |:-- |:-- | +| bashTests | python | tests/bash_tests | $ ctest -R bash | -DEXIV2_BUILD_SAMPLES=ON | +| bugfixTests | python | tests/bugfixes | $ ctest -R bugfix | -DEXIV2_ENBALE_VIDEO=ON | +| lensTest | C++ | tests/lens_tests | $ ctest -R lens | | +| tiffTests | python | tests/tiff_test | $ ctest -R tiff | | +| unitTests | C++ | unitTests/ | $ ctest -R unit | -DEXIV2_BUILD_UNIT_TESTS=ON | +| versionTests | C++ | src/version.cpp | $ ctest -R version | Always in library | + +The term _**bashTests**_ is historical. These tests were originally bash scripts and have been rewritten in python. +Visual Studio Users will appreciate the python implementation as it avoids the installation of mingw/cygwin and special PATH settings. + +If you build the code in the directory `/build`, tests will run using the default values of Environment Variables. + +[TOC](#TOC) +
+ +## Exiv2 Environment Variables + +Exiv2 optionally uses several different environment variables when building or testing. + +| Variable | Default | Platforms | Purpose | +|:-- |:-- |:-- |:-- | +| EXIV2_BINDIR | **\/build/bin** | All Platforms | Path of built binaries (e.g., exiv2.exe) | +| EXIV2_PORT | **12762**
**12671**
**12760** | Cygwin
MinGW/msys2
Other Platforms | Test TCP/IP Port | +| EXIV2_HTTP | **http://localhost** | All Platforms | Test http server | +| EXIV2_ECHO | _**not set**_ | All Platforms | For debugging bashTests | +| VALGRIND | _**not set**_ | All Platforms | For debugging bashTests | +| VERBOSE | _**not set**_ | Makefile platforms | Instructs make to report its actions | | PATH
DYLD\_LIBRARY\_PATH
LD\_LIBRARY\_PATH | $EXIV2\_BINDIR/../lib | Windows
macOS
Other platforms | Path of dynamic libraries | The Variable EXIV2\_PORT or EXIV2\_HTTP can be set to None to skip http tests. The http server is started with the command `python3 -m http.server $port`. On Windows, you will need to run this manually _**once**_ to authorise the firewall to permit python to use the port. [TOC](#TOC) -
-### 4.1 Running tests on a UNIX-like system +
+ +## Running tests on Unix-like systems You can run tests directly from the build: ```bash -$ cmake .. -G "Unix Makefiles" -DEXIV2_BUILD_UNIT_TESTS=On -$ make -... lots of output ... -$ make tests +$ cmake -S . -B build -G "Unix Makefiles" -DEXIV2_BUILD_UNIT_TESTS=ON +... lots of output and build summary ... +$ cmake --build build ... lots of output ... +$ ctest --test-dir build +... test summary ... $ ``` You can run individual tests in the `test` directory. **Caution:** If you build in a directory other than \/build, you must set EXIV2\_BINDIR to run tests from the `test` directory. ```bash -$ cd /build -$ make bash_tests +$ cd +$ ctest --test-dir build -R bash --verbose addmoddel_test (testcases.TestCases) ... ok .... Ran 176 tests in 9.526s OK (skipped=6) -$ make python_tests +$ ctest --test-dir build -R bugfix --verbose ... lots of output ... test_run (tiff_test.test_tiff_test_program.TestTiffTestProg) ... ok ---------------------------------------------------------------------- @@ -894,55 +1028,60 @@ $ ``` [TOC](#TOC) -
-### 4.2 Running tests on Visual Studio builds from cmd.exe +
+ +## Running tests on Visual Studio builds from cmd.exe -**Caution:** _The python3 interpreter must be on the PATH, build for DOS, and called python3.exe. I copied the python.exe program: +**Caution:** _The python3 interpreter must be on the PATH, build for DOS, and called python3.exe._ I copied the python.exe program: ```cmd > copy c:\Python37\python.exe c:\Python37\python3.exe -> set "PATH=c:\Python37;%PATH% +> set PATH=c:\Python37;%PATH% ``` -You can execute the test suite as described for UNIX-like systems. -The main difference is that you must use cmake to initiate the test -as make is not a system utility on Windows. +You can execute the test suite in a similar manner to that described for UNIX-like systems. You _**must**_ provide the `-C` config option to ctest for Visual Studio builds. -```bash -> cd /build -> cmake --build . --target tests -> cmake --build . --target python_tests +```cmd +> cd +> ctest --test-dir build -C Release +> ctest --test-dir build -C Release -R bugfix --verbose +``` +Visual Studio can build different configs as follows: + +```cmd +> cmake --build build --config Release # or Debug or MinSizeRel or RelWithDebInfo +> ctest --test-dir build -C Release ``` +The default for **CMake** config option `--config` is `Release`. **ctest** does not have a default for config option `-C`. -##### Running tests from cmd.exe +### Running tests from cmd.exe You can build with Visual Studio using Conan. The is described in detail in [README-CONAN.md](README-CONAN.md) As a summary, the procedure is: ``` -c:\...\exiv2>mkdir build -c:\...\exiv2>cd build -c:\...\exiv2\build>conan install .. --build missing --profile msvc2019Release -c:\...\exiv2\build>cmake .. -DEXIV2_BUILD_UNIT_TESTS=On -G "Visual Studio 16 2019" -c:\...\exiv2\build>cmake --build . --config Release +c:\...\exiv2>conan install . --build missing --profile msvc2019Release +c:\...\exiv2>cmake -S . B build -DEXIV2_BUILD_UNIT_TESTS=ON -G "Visual Studio 16 2019" +c:\...\exiv2>cmake --build build --config Release ... lots of output from compiler and linker ... -c:\...\exiv2\build> +c:\...\exiv2>ctest --test-dir build -C Release ``` If you wish to use an environment variables, use set: ``` -set VERBOSE=1 -cmake --build . --config Release --target tests -set VERBOSE= +set EXIV2_PORT=54321 +ctest --test-dir build -C Release --verbose -R bash +set EXIV2_PORT= ``` [TOC](#TOC) -
-### 4.3 Unit tests +
+ +## Unit Tests -The code for the unit tests is in `/unitTests`. To include unit tests in the build, use the *cmake* option `-DEXIV2_BUILD_UNIT_TESTS=On`. +The code for the unit tests is in `/unitTests`. To include unit tests in the build, use the *CMake* option `-DEXIV2_BUILD_UNIT_TESTS=ON`. There is a discussion on the web about installing GTest: [https://github.com/Exiv2/exiv2/issues/575](https://github.com/Exiv2/exiv2/issues/575) @@ -958,24 +1097,25 @@ $ popd ``` [TOC](#TOC) -
-### 4.4 Python tests +
-You can run the python tests from the build directory: +## Bugfix Tests + +You can run the bugfix tests from the build directory: ```bash -$ cd /build -$ make python_tests +$ cd +$ ctest --test-dir build -R bugfix ``` If you wish to run in verbose mode: ```bash -$ cd /build -$ make python_tests VERBOSE=1 +$ cd +$ ctest --test-dir build -R bugfix --verbose ``` -The python tests are stored in the directory tests and you can run them all with the command: +The bugfix tests are stored in directory tests/ and you can run them all with the command: ```bash $ cd /tests @@ -993,39 +1133,64 @@ $ python3 runner.py --verbose bugfixes/redmine/test_issue_841.py # or $(find . You may wish to get a brief summary of failures with commands such as: ```bash -$ cd /build -$ make python_tests 2>&1 | grep FAIL +$ cd +$ ctest --test-dir build -R bugfix --verbose 2>&1 | grep FAIL +``` + +[TOC](#TOC) +
+ +## Fuzzing + +The code for the fuzzers is in `exiv2dir/fuzz` + +To build the fuzzers, use the *cmake* option `-DEXIV2_BUILD_FUZZ_TESTS=ON` and `-DEXIV2_TEAM_USE_SANITIZERS=ON`. +Note that it only works with clang compiler as libFuzzer is integrated with clang > 6.0 + +To build the fuzzers: + +```bash +$ cd +$ rm -rf build-fuzz +$ cmake -S . -B build-fuzz -DCMAKE_CXX_COMPILER=$(which clang++) -DEXIV2_BUILD_FUZZ_TESTS=ON -DEXIV2_TEAM_USE_SANITIZERS=ON +$ cmake --build build-fuzz ``` +To execute a fuzzer: + +```bash +cd /build-fuzz +mkdir corpus +./bin/fuzz-read-print-write corpus ../test/data/ -jobs=$(nproc) -workers=$(nproc) -max_len=4096 +``` + +For more information about fuzzing see [`fuzz/README.md`](fuzz/README.md). + [TOC](#TOC) -
-### 4.5 Test Summary +
+ +### OSS-Fuzz -| *Tests* | Unix Style Platforms _(bash)_ | Visual Studio _(cmd.exe)_ | -|:-- |:--- |:-- | -| | $ cd \/build | \> cd \/build | -| tests | $ make tests | \> cmake --build . --config Release --target tests | -| bash_tests | $ make bash_tests | \> cmake --build . --config Release --target bash_tests | -| python_tests | $ make python_tests | \> cmake --build . --config Release --target python_tests | -| unit_test | $ make unit_test | \> cmake --build . --config Release --target unit_test | -| version_test | $ make version_test | \> cmake --build . --config Release --target version_test | +Exiv2 is enrolled in [OSS-Fuzz](https://google.github.io/oss-fuzz/), which is a fuzzing service for open-source projects, run by Google. -The name **bash_tests** is historical. They are implemented in python. +The build script used by OSS-Fuzz to build Exiv2 can be found [here](https://github.com/google/oss-fuzz/tree/master/projects/exiv2/build.sh). It uses the same fuzz target ([`fuzz-read-print-write`](fuzz/fuzz-read-print-write.cpp)) as mentioned above, but with a slightly different build configuration to integrate with OSS-Fuzz. In particular, it uses the CMake option `-DEXIV2_TEAM_OSS_FUZZ=ON`, which builds the fuzz target without adding the `-fsanitize=fuzzer` flag, so that OSS-Fuzz can control the sanitizer flags itself. [TOC](#TOC) -
-## 5 Platform Notes +
+ +# Platform Notes There are many ways to set up and configure your platform. The following notes are provided as a guide. -
-### 5.1 Linux +
+ +## Linux Update your system and install the build tools and dependencies (zlib, expat, gtest and others) ```bash $ sudo apt --yes update -$ sudo apt install --yes build-essential git clang ccache python3 libxml2-utils cmake python3 libexpat1-dev libz-dev zlib1g-dev libssh-dev libcurl4-openssl-dev libgtest-dev google-mock +$ sudo apt install --yes build-essential ccache clang cmake git google-mock libbrotli-dev libcurl4-openssl-dev libexpat1-dev libgtest-dev libinih-dev libssh-dev libxml2-utils libz-dev python3 zlib1g-dev ``` For users of other platforms, the script /ci/install_dependencies.sh has code used to configure many platforms. The code in that file is a useful guide to configuring your platform. @@ -1037,14 +1202,14 @@ $ mkdir -p ~/gnu/github/exiv2 $ cd ~/gnu/github/exiv2 $ git clone https://github.com/exiv2/exiv2 $ cd exiv2 -$ mkdir build ; cd build ; -$ cmake .. -G "Unix Makefiles" -$ make +$ cmake -S . -B build -G "Unix Makefiles" +$ cmake --build build ``` [TOC](#TOC) -
-### 5.2 macOS +
+ +## macOS You will need to install Xcode and the Xcode command-line tools to build on macOS. @@ -1053,71 +1218,53 @@ You should build and install libexpat and zlib. You may use brew, macports, bui I recommend that you build and install CMake from source. [TOC](#TOC) -
-### 5.3 MinGW/msys2 +
-Please note that the platform MinGW/msys2 32 is obsolete and superceded by MinGW/msys2 64. +## MinGW/msys2 -#### MinGW/msys2 64 bit -Install: [https://repo.msys2.org/distrib/x86\_64/msys2-x86\_64-20200903.exe](https://repo.msys2.org/distrib/x86_64/msys2-x86_64-20200903.exe) +Please note that the 32bit MinGW platform is obsolete and superceded by the 64bit MSYS2 distribution. It is important to highlight that we rely on using the Universal C Runtime (UCRT) and its relatively new support for UTF-8. Check this [PR](https://github.com/Exiv2/exiv2/pull/2090) for more information. Therefore you will need to use the [MSYS2 URCT64 environment](https://www.msys2.org/docs/environments/). -The file `appveyor_mingw_cygwin.yml` has instructions to configure the AppVeyor CI to configures itself to build Exiv2 on MinGW/msys2 and Cygwin/64. +Install the latest version of [MSYS2](https://repo.msys2.org/distrib/msys2-x86_64-latest.exe), and follow the installation instructions available [here](https://www.msys2.org/). -I use the following batch file to start the MinGW/msys2 64 bit bash shell from the Dos Command Prompt (cmd.exe) +The CI workflow file `.github/workflows/on_PR_windows_matrix.yml` has a build job named `msys2` with instructions showing how to configure Exiv2 on MSYS2. -```bat -@echo off -setlocal -set "PATH=c:\msys64\mingw64\bin;c:\msys64\usr\bin;c:\msys64\usr\local\bin;" -set "PS1=\! MSYS \u@\h:\w \$ " -set "HOME=c:\msys64\home\rmills" -if NOT EXIST %HOME% mkdir %HOME% -cd %HOME% -color 1f -c:\msys64\usr\bin\bash.exe -norc -endlocal - -``` - -#### Install MinGW Dependencies +### Install exiv2 Dependencies -Install tools and dependencies: +Please note that you will need to install the `ucrt-x86_64` package version of the exiv2 dependencies: ```bash -for i in base-devel git coreutils dos2unix tar diffutils make \ - mingw-w64-x86_64-toolchain mingw-w64-x86_64-gcc mingw-w64-x86_64-gdb \ - mingw-w64-x86_64-cmake mingw-w64-x86_64-gettext mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-libexpat mingw-w64-x86_64-libiconv mingw-w64-x86_64-zlib \ - mingw-w64-x86_64-gtest -do (echo y | pacman -S $i) ; done -``` - -#### Download exiv2 from github and build - -```bash -$ mkdir -p ~/gnu/github/exiv2 -$ cd ~/gnu/github/exiv2 -$ git clone https://github.com/exiv2/exiv2 -$ cd exiv2 -$ mkdir build ; cd build ; -$ cmake .. -G "Unix Makefiles" # or "MSYS Makefiles" -$ make +pacman -S --needed mingw-w64-ucrt-x86_64-{brotli,cc,cmake,curl,expat,gettext,gtest,libiconv,libwinpthread,ninja,zlib} ``` -#### MinGW and Regex +### Download exiv2 from github and build -The exiv2 command-line program provides an option **`--grep`** to filter output. The implementation requires the header file **``** and supporting library to be available during the build. When not available, the option **`--grep`** degrades to a substring match. Because there are several versions of **``** available on the MinGW platform, detection of regex is always disabled on this platform and uses substring match. The following command reveals if regex is included in your build: +Use the Windows start menu to open the terminal customized for the UCRT64 environment: `MSYS2 MinGW UCRT x64`. Then run the following commands to download exiv2, configure the project and build it: ```bash -$ exiv2 -vVg regex -exiv2 0.27.1 -have_regex=1 -$ -``` +mkdir -p ~/gnu/github/exiv2 +cd ~/gnu/github/exiv2 +git clone https://github.com/exiv2/exiv2 +cd exiv2 +cmake -S . -B build + -G Ninja + -DCMAKE_CXX_FLAGS=-Wno-deprecated + -DCMAKE_BUILD_TYPE=Release + -DBUILD_SHARED_LIBS=ON + -DEXIV2_BUILD_SAMPLES=ON + -DEXIV2_ENABLE_NLS=OFF + -DEXIV2_ENABLE_WEBREADY=ON + -DEXIV2_ENABLE_BMFF=ON + -DEXIV2_BUILD_UNIT_TESTS=ON + +cmake --build build +``` + +The binaries generated at this point can be executed from the MSYS2 UCRT64 terminal, but they will not run from a Windows Command Prompt or PowerShell. The reason is that the MSYS2 UCRT64 terminal is properly configured to find some needed DLLs. In case you want to be able to run the generated **exiv2** binary from any Windows terminal, you'll need to deploy the needed DLLs with the application. [TOC](#TOC) -
-### 5.4 Cygwin/64 +
+ +## Cygwin/64 Please note that the platform Cygwin/32 is obsolete and superceded by Cygwin/64. @@ -1126,9 +1273,9 @@ Download: [https://cygwin.com/install.html](https://cygwin.com/install.html) and You need: make, cmake, curl, gcc, gettext-devel pkg-config, dos2unix, tar, zlib-devel, libexpat1-devel, git, libxml2-devel python3-interpreter, libiconv, libxml2-utils, libncurses, libxml2-devel libxslt-devel python38 python38-pip python38-libxml2 -The file `appveyor_mingw_cygwin.yml` has instructions to configure the AppVeyor CI to configures itself to build Exiv2 on MinGW/msys2 and Cygwin/64. +The CI workflow file `.github/workflows/on_PR_windows_matrix.yml` has a build job named `cygwin` with instructions showing how to configure Exiv2 on Cygwin/64. -To build unit tests, you should install googletest-release-1.8.0 as discussed [4.3 Unit tests](#4-3) +To build unit tests, you should install googletest-release-1.8.0 as discussed at [Unit tests](#UnitTests) I use the following batch file "cygwin64.bat" to start the Cygwin/64 bash shell from the Dos Command Prompt (cmd.exe). @@ -1145,10 +1292,11 @@ endlocal ``` [TOC](#TOC) -
-### 5.5 Visual Studio +
-We recommend that you use Conan to build Exiv2 using Visual Studio. Exiv2 v0.27 can be built with Visual Studio versions 2008 and later. We actively support and build with Visual Studio 2015, 2017 and 2019. +## Visual Studio + +We recommend that you use Conan to get the Exiv2 dependencies when using Visual Studio. Exiv2 v0.27 can be built with Visual Studio versions 2008 and later. For the `main` branch we actively support and build with Visual Studio 2019 and 2022. As well as Visual Studio, you will need to install CMake, Python3, and Conan. @@ -1163,9 +1311,12 @@ As well as Visual Studio, you will need to install CMake, Python3, and Conan. The python3 interpreter must be on your PATH. +It is important to highlight that we rely on using of the Universal C Runtime (UCRT) and its relatively new support for UTF-8. Check this [PR](https://github.com/Exiv2/exiv2/pull/2090) for more information. + [TOC](#TOC) -
-### 5.6 Unix +
+ +## Unix Exiv2 can be built on many Unix and Linux distros. With v0.27.2, we are starting to actively support the Unix Distributions NetBSD and FreeBSD. For v0.27.3, I have added support for Solaris 11.4 @@ -1188,7 +1339,7 @@ To run the test suite, you need: 3. dos2unix 4. xmllint -#### NetBSD +### NetBSD You can build exiv2 from source using the methods described for linux. I built and installed exiv2 using "Pure CMake" and didn't require conan. @@ -1205,9 +1356,9 @@ It's important to ensure that `LD_LIBRARY_PATH` includes `/usr/local/lib` and `/ It's important to ensure that `PATH` includes `/usr/local/bin`, `/usr/pkg/bin` and `/usr/pkg/sbin`. -#### FreeBSD +### FreeBSD -Clang is pre-installed as ``/usr/bin/{cc|c++}` as well as libz and expat. FreeBSD uses pkg as the package manager which I used to install cmake and git. +Clang is pre-installed as ``/usr/bin/{cc|c++}` as well as libz and expat. FreeBSD uses pkg as the package manager which I used to install CMake and git. ```bash $ su root @@ -1237,7 +1388,7 @@ Error updating repositories! 635 rmills@rmillsmm-freebsd:~/gnu/github/exiv2/0.27-maintenance/build $ ``` -#### Solaris +### Solaris Solaris uses the package manager pkg. To get a list of packages: @@ -1251,8 +1402,6 @@ To install a package: $ sudo pkg install developer/gcc-7 ``` - [TOC](#TOC) -Written by Robin Mills
robin@clanmills.com
Updated: 2021-04-06 - +Updated: 2026-02-24 diff --git a/SECURITY.md b/SECURITY.md index 83b3a3a6de..85aac4d235 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,21 +2,36 @@ ## Supported Versions -| Exiv2 Version | Branch | _Dot_ or _Security_ Releases | -|:-- |:-- |:-- | -| v0.27 | 0.27-maintenance | v0.27.1
v0.27.2
v0.27.3 | -| v0.26 | Branch 0.26 | None | -| v0.25 | Branch 0.25 | None | +| Exiv2 Version | Date | Tag | Branch | _Dot/Security_ Release | Date | Tag | +|:-- |:-- |:- |:-- |:-- |:- |:- | +| v0.28 | 2023-05-08 | v0.28.0 | 0.28.x | v0.28.8 | 2026-03-01 | v0.28.8 | ## Security Process -Security alerts are published here: https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=Exiv2 We open an issue with the label "Security" on GitHub and fix it. It doesn't get special treatment and will be included in the next release of the branch. +If you have found a security vulnerability in Exiv2, please follow these steps: + +* Click [this link](https://github.com/Exiv2/exiv2/security/advisories/new) to create a draft security advisory. +* Write a detailed description of the vulnerability in the draft advisory. +* Include all of the following details in your description of the vulnerability: + * Exact version of Exiv2 that you tested. _For example: commit [194bb65ac568a5435874c9d9d73b1c8a68e4edec](https://github.com/Exiv2/exiv2/commit/194bb65ac568a5435874c9d9d73b1c8a68e4edec)_ + * Platform used. _For example: Ubuntu 22.04.3 LTS (x86\_64)_ + * Exact command used to build Exiv2. _For example: `mkdir build; cd build; cmake ..; make`_ + * Attach a copy of the image file that triggers the bug. _For example: `poc.jpg`_ + * Exact command line arguments that trigger the bug. _For example: `./bin/exiv2 poc.jpg`_ + * Crash output (stdout + stderr). + * The source location of the bug and/or any other information that you are able to provide about what the cause of the bug is. + +The draft security advisory is private until we publish it, so it is a good place to discuss the details of the vulnerability privately. + +To qualify as a security issue, the bug **must** be reproducible on an official release of Exiv2, via a realistic attack vector. As a general rule, that means it should be possible to trigger the bug by running the `exiv2` command-line application on a malicious input file. Please note that the applications in the `samples` sub-directory are demo applications that are not intended for production use, so we usually do not consider bugs in those applications to be security vulnerabilities. However, if one of the sample applications reveals a legitimate bug in the exiv2 library then we will still consider it as a potential security issue. + +Official releases are listed [here](https://github.com/Exiv2/exiv2/releases) (not including those labeled "pre-release"). Bugs that are only reproducible on the [main branch](https://github.com/Exiv2/exiv2/tree/main) or on a pre-release are not security issues and can be reported as regular [issues](https://github.com/Exiv2/exiv2/issues). Team Exiv2 does not back-port security (or any other fix) to earlier releases of the code. An engineer at SUSE has patched and fixed some security releases for Exiv2 v0.26 and Exiv2 v0.25 in branches 0.26 and 0.25. Exiv2 has provided several _**Dot Release**_ for v0.27. Exiv2 has never issued a _**Security Release**_. -The version numbering scheme is explained below. The design includes provision for a security release. A _**Dot Release**_ is an updated version of the library with security PRs and other changes. A _**Dot Release**_ offers the same API as its parent. A _**Security Release**_ is an existing release PLUS one or more security PRs. Nothing else is changed from it parent. +The version numbering scheme is explained below. The design includes provision for a security release. A _**Dot Release**_ is an updated version of the library with security PRs and other changes. A _**Dot Release**_ offers the same API as its parent. A _**Security Release**_ is an existing release PLUS one or more security PRs. Nothing else is changed from it parent. -Users can register on GitHub.com to receive release notices for RC and GM Releases. Additionally, we inform users when we begin a project to create a new release on FaceBook (https://facebook.com/exiv2) and Discuss Pixls (https://discuss.pixls.us). The announcement of a new release project has a preliminay specification and schedule. +Users can register on GitHub.com to receive release notices for RC and GM Releases. Additionally, we inform users when we begin a project to create a new release on FaceBook (https://facebook.com/exiv2) and Discuss Pixls (https://discuss.pixls.us). The announcement of a new release project has a preliminary specification and schedule. ## Version Numbering Scheme @@ -25,15 +40,11 @@ Users can register on GitHub.com to receive release notices for RC and GM Releas | v0.27.7.3 | Exiv2 v0.27.3 | GM | Golden Master. This is the final and official release. | | v0.27.3.2 | Exiv2 v0.27.3.2 | RC2 | Release Candidate 2. | | v0.27.3.20 | Exiv2 v0.27.3.2 | RC2 Preview | Dry-run for release candidate. For team review. | -| v0.27.3.81 | Exiv2 v0.27.3 | Security Fix | Security Release | +| v0.27.3.81 | Exiv2 v0.27.3 | Security Fix | Security Release | | v0.27.3.29 | Exiv2 v0.27.3.29 | Development | Should never be installed for production. | | v0.27.4.9 | Exiv2 v0.27.4.9 | Development | Should never be installed for production. | | v0.27.99 | Exiv2 v0.28 | Development | Should never be installed for production. | +## Fixed CVEs -## Reported CVEs - -| CVE | Description | Solution | PR | -|:-- |:-- |:-- |:-- | -| [CVE-2019-9144](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9144) | Crash in BigTiffImage::printIFD | Remove src/bigtiffimage.cpp | [#1331](https://github.com/Exiv2/exiv2/pull/1331) | -| to be continued | | | | +The list of Exiv2 vulnerabilities that were previously found and fixed can be found at https://github.com/Exiv2/exiv2/security/advisories. diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000000..4a053c3872 --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,51 @@ +set(APP_SOURCES + exiv2.cpp + actions.cpp + getopt.cpp + app_utils.cpp +) + +add_executable(exiv2 ${APP_SOURCES}) + +# Make app use UTF-8 code page in Windows +if(WIN32) + if(MSVC) + target_sources(exiv2 PRIVATE utf8.manifest) + else() + # Must wrap manifest in .rc w/ other toolchains + target_sources(exiv2 PRIVATE utf8.rc) + endif() +endif() + +target_include_directories(exiv2 PRIVATE ${PROJECT_SOURCE_DIR}/src) # To find i18n.hpp + +set_target_properties(exiv2 PROPERTIES COMPILE_FLAGS ${EXTRA_COMPILE_FLAGS} XCODE_ATTRIBUTE_GCC_GENERATE_DEBUGGING_SYMBOLS[variant=Debug] "YES") +if(MSVC) + set_target_properties(exiv2 PROPERTIES LINK_FLAGS "/ignore:4099") # Ignore missing PDBs +endif() + +target_link_libraries(exiv2 PRIVATE exiv2lib) + +if(EXIV2_ENABLE_NLS) + target_link_libraries(exiv2 PRIVATE ${Intl_LIBRARIES}) + target_include_directories(exiv2 PRIVATE ${Intl_INCLUDE_DIRS}) +endif() + +if(USING_CONAN + AND WIN32 + AND EXISTS ${PROJECT_BINARY_DIR}/conanDlls +) + # In case of using conan recipes with their 'shared' option turned on, we will have dlls of the 3rd party dependencies in the conanDlls folder. + + add_custom_command( + TARGET exiv2 + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory ${PROJECT_BINARY_DIR}/conanDlls ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + COMMENT "Copy 3rd party DLLs the bin folder" + ) + + # Copy 3rd party DLLs the bin folder. [install step] + install(DIRECTORY ${PROJECT_BINARY_DIR}/conanDlls/ DESTINATION bin) +endif() + +install(TARGETS exiv2 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/app/actions.cpp b/app/actions.cpp new file mode 100644 index 0000000000..b597b60439 --- /dev/null +++ b/app/actions.cpp @@ -0,0 +1,1994 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +// included header files +#include "actions.hpp" + +#include "app_utils.hpp" +#include "config.h" +#include "easyaccess.hpp" +#include "enforce.hpp" +#include "exif.hpp" +#include "futils.hpp" +#include "i18n.h" // NLS support. +#include "image.hpp" +#include "iptc.hpp" +#include "preview.hpp" +#include "safe_op.hpp" +#include "xmp_exiv2.hpp" + +#include +#include +#include +#include +#include +#include + +// + standard includes +#include // for stat() +#if __has_include() +#include // for stat() +#endif + +#ifdef _WIN32 +#include +#include +#include +#include +#else +#include +#endif + +#ifndef _WIN32 +#define _setmode(a, b) \ + do { \ + } while (false) +#define _fileno fileno +#endif + +namespace fs = std::filesystem; + +// ***************************************************************************** +// local declarations +namespace { +std::mutex cs; + +//! Helper class to set the timestamp of a file to that of another file +class Timestamp { + public: + //! C'tor + int read(const std::string& path); + //! Read the timestamp from a broken-down time in buffer \em tm. + int read(tm* tm); + //! Set the timestamp of a file + int touch(const std::string& path) const; + + private: + time_t actime_{0}; + time_t modtime_{0}; +}; + +/*! + @brief Convert a string "YYYY:MM:DD HH:MI:SS" to a tm type, + returns 0 if successful + */ +int str2Tm(const std::string& timeStr, tm* tm); + +//! Convert a localtime to a string "YYYY:MM:DD HH:MI:SS", "" on error +std::string time2Str(time_t time); + +/*! + @brief Copy metadata from source to target according to Params::copyXyz + + @param source Source file path + @param tgt Target file path. An *.exv file is created if target doesn't + exist. + @param targetType Image type for the target image in case it needs to be + created. + @param preserve Indicates if existing metadata in the target file should + be kept. + @return 0 if successful, else an error code +*/ +int metacopy(const std::string& source, const std::string& tgt, Exiv2::ImageType targetType, bool preserve); + +/*! + @brief Rename a file according to a timestamp value. + + @param path The original file path. Contains the new path on exit. + @param tm Pointer to a buffer with the broken-down time to rename + the file to. + @return 0 if successful, -1 if the file was skipped, 1 on error. +*/ +int renameFile(std::string& path, const tm* tm, Exiv2::ExifData& exifData); + +/*! + @brief Make a file path from the current file path, destination + directory (if any) and the filename extension passed in. + + @param path Path of the existing file + @param ext New filename extension (incl. the dot '.' if required) + @return 0 if successful, 1 if the new file exists and the user + chose not to overwrite it. + */ +std::string newFilePath(const std::string& path, const std::string& ext); + +/*! + @brief Check if file \em path exists and whether it should be + overwritten. Ask user if necessary. Return 1 if the file + exists and shouldn't be overwritten, else 0. + */ +int dontOverwrite(const std::string& path); + +/*! + @brief Output a text with a given minimum number of chars, honoring + multi-byte characters correctly. Replace code in the form + os << setw(width) << myString + with + os << pair( myString, width) + */ +std::ostream& operator<<(std::ostream& os, const std::pair& strAndWidth); + +//! Print image Structure information +int printStructure(std::ostream& out, Exiv2::PrintStructureOption option, const std::string& path); +} // namespace + +// ***************************************************************************** +// class member definitions +namespace Action { +TaskFactory& TaskFactory::instance() { + static TaskFactory instance_; + return instance_; +} + +void TaskFactory::cleanup() { + registry_.clear(); +} + +TaskFactory::TaskFactory() { + registry_.emplace(adjust, std::make_unique()); + registry_.emplace(print, std::make_unique()); + registry_.emplace(rename, std::make_unique()); + registry_.emplace(erase, std::make_unique()); + registry_.emplace(extract, std::make_unique()); + registry_.emplace(insert, std::make_unique()); + registry_.emplace(modify, std::make_unique()); + registry_.emplace(fixiso, std::make_unique()); + registry_.emplace(fixcom, std::make_unique()); +} + +Task::UniquePtr TaskFactory::create(TaskType type) { + auto i = registry_.find(type); + if (i != registry_.end() && i->second) { + return i->second->clone(); + } + return nullptr; +} + +static int setModeAndPrintStructure(Exiv2::PrintStructureOption option, const std::string& path, bool binary) { + int result = 0; + if (binary && option == Exiv2::kpsIccProfile) { + std::stringstream output(std::stringstream::out | std::stringstream::binary); + result = printStructure(output, option, path); + std::string str = output.str(); + if (result == 0 && !str.empty()) { + Exiv2::DataBuf iccProfile(str.size()); + Exiv2::DataBuf ascii((str.size() * 3) + 1); + ascii.write_uint8(str.size() * 3, 0); + std::copy(str.begin(), str.end(), iccProfile.begin()); + if (Exiv2::base64encode(iccProfile.c_data(), str.size(), reinterpret_cast(ascii.data()), str.size() * 3)) { + const size_t chunk = 60; + std::string code = std::string("data:") + ascii.c_str(); + size_t length = code.size(); + for (size_t start = 0; start < length; start += chunk) { + auto count = std::min(chunk, length - start); + std::cout << code.substr(start, count) << '\n'; + } + } + } + } else { + _setmode(_fileno(stdout), O_BINARY); + result = printStructure(std::cout, option, path); + } + + return result; +} + +int Print::run(const std::string& path) { + try { + path_ = path; + switch (Params::instance().printMode_) { + case Params::pmSummary: + return Params::instance().greps_.empty() ? printSummary() : printList(); + case Params::pmList: + return printList(); + case Params::pmComment: + return printComment(); + case Params::pmPreview: + return printPreviewList(); + case Params::pmStructure: + return printStructure(std::cout, Exiv2::kpsBasic, path_); + case Params::pmRecursive: + return printStructure(std::cout, Exiv2::kpsRecursive, path_); + case Params::pmXMP: + return setModeAndPrintStructure(Exiv2::kpsXMP, path_, binary()); + case Params::pmIccProfile: + return setModeAndPrintStructure(Exiv2::kpsIccProfile, path_, binary()); + } + return 0; + } catch (const Exiv2::Error& e) { + std::cerr << "Exiv2 exception in print action for file " << path << ":\n" << e << "\n"; + return 1; + } catch (const std::overflow_error& e) { + std::cerr << "std::overflow_error exception in print action for file " << path << ":\n" << e.what() << "\n"; + return 1; + } +} + +int Print::printSummary() { + if (!Exiv2::fileExists(path_)) { + std::cerr << path_ << ": " << _("Failed to open the file") << "\n"; + return -1; + } + + auto image = Exiv2::ImageFactory::open(path_); + image->readMetadata(); + const Exiv2::ExifData& exifData = image->exifData(); + align_ = 16; + + // Filename + printLabel(_("File name")); + std::cout << path_ << '\n'; + + // Filesize + printLabel(_("File size")); + std::cout << fs::file_size(path_) << " " << _("Bytes") << '\n'; + + // MIME type + printLabel(_("MIME type")); + std::cout << image->mimeType() << '\n'; + + // Image size + printLabel(_("Image size")); + std::cout << image->pixelWidth() << " x " << image->pixelHeight() << '\n'; + + if (exifData.empty()) { + std::cerr << path_ << ": " << _("No Exif data found in the file") << "\n"; + return -3; + } + + // Thumbnail + printLabel(_("Thumbnail")); + Exiv2::ExifThumbC exifThumb(exifData); + std::string thumbExt = exifThumb.extension(); + if (thumbExt.empty()) { + std::cout << _("None"); + } else { + auto dataBuf = exifThumb.copy(); + if (dataBuf.empty()) { + std::cout << _("None"); + } else { + std::cout << exifThumb.mimeType() << ", " << dataBuf.size() << " " << _("Bytes"); + } + } + std::cout << '\n'; + + printTag(exifData, Exiv2::make, _("Camera make")); + printTag(exifData, Exiv2::model, _("Camera model")); + printTag(exifData, Exiv2::dateTimeOriginal, _("Image timestamp")); + printTag(exifData, "Exif.Canon.FileNumber", _("File number")); + printTag(exifData, Exiv2::exposureTime, _("Exposure time"), Exiv2::shutterSpeedValue); + printTag(exifData, Exiv2::fNumber, _("Aperture"), Exiv2::apertureValue); + printTag(exifData, Exiv2::exposureBiasValue, _("Exposure bias")); + printTag(exifData, Exiv2::flash, _("Flash")); + printTag(exifData, Exiv2::flashBias, _("Flash bias")); + printTag(exifData, Exiv2::focalLength, _("Focal length")); + printTag(exifData, Exiv2::subjectDistance, _("Subject distance")); + printTag(exifData, Exiv2::isoSpeed, _("ISO speed")); + printTag(exifData, Exiv2::exposureMode, _("Exposure mode")); + printTag(exifData, Exiv2::meteringMode, _("Metering mode")); + printTag(exifData, Exiv2::macroMode, _("Macro mode")); + printTag(exifData, Exiv2::imageQuality, _("Image quality")); + printTag(exifData, Exiv2::whiteBalance, _("White balance")); + printTag(exifData, "Exif.Image.Copyright", _("Copyright")); + printTag(exifData, "Exif.Photo.UserComment", _("Exif comment")); + + std::cout << '\n'; + + return 0; +} // Print::printSummary + +void Print::printLabel(const std::string& label) const { + std::cout << std::setfill(' ') << std::left; + if (Params::instance().files_.size() > 1) { + std::cout << std::setw(20) << path_ << " "; + } + std::cout << std::pair(label, align_) << ": "; +} + +int Print::printTag(const Exiv2::ExifData& exifData, const std::string& key, const std::string& label) const { + int rc = 0; + if (!label.empty()) { + printLabel(label); + } + Exiv2::ExifKey ek(key); + auto md = exifData.findKey(ek); + if (md != exifData.end()) { + md->write(std::cout, &exifData); + rc = 1; + } + if (!label.empty()) + std::cout << '\n'; + return rc; +} // Print::printTag + +int Print::printTag(const Exiv2::ExifData& exifData, EasyAccessFct easyAccessFct, const std::string& label, + EasyAccessFct easyAccessFctFallback) const { + int rc = 0; + if (!label.empty()) { + printLabel(label); + } + auto md = easyAccessFct(exifData); + if (md != exifData.end()) { + md->write(std::cout, &exifData); + rc = 1; + } else if (easyAccessFctFallback) { + md = easyAccessFctFallback(exifData); + if (md != exifData.end()) { + md->write(std::cout, &exifData); + rc = 1; + } + } + if (!label.empty()) + std::cout << '\n'; + return rc; +} // Print::printTag + +int Print::printList() { + if (!Exiv2::fileExists(path_)) { + std::cerr << path_ << ": " << _("Failed to open the file") << "\n"; + return -1; + } + + auto image = Exiv2::ImageFactory::open(path_); + image->readMetadata(); + // Set defaults for metadata types and data columns + if (Params::instance().printTags_ == MetadataId::invalid) { + Params::instance().printTags_ = MetadataId::exif | MetadataId::iptc | MetadataId::xmp; + } + if (Params::instance().printItems_ == 0) { + Params::instance().printItems_ = Params::prKey | Params::prType | Params::prCount | Params::prTrans; + } + return printMetadata(image.get()); +} // Print::printList + +int Print::printMetadata(const Exiv2::Image* image) { + bool ret = false; + bool noExif = false; + if ((Params::instance().printTags_ & MetadataId::exif) == MetadataId::exif) { + const Exiv2::ExifData& exifData = image->exifData(); + for (auto&& md : exifData) { + ret |= printMetadatum(md, image); + } + if (exifData.empty()) + noExif = true; + } + + bool noIptc = false; + if ((Params::instance().printTags_ & MetadataId::iptc) == MetadataId::iptc) { + const Exiv2::IptcData& iptcData = image->iptcData(); + for (auto&& md : iptcData) { + ret |= printMetadatum(md, image); + } + if (iptcData.empty()) + noIptc = true; + } + + bool noXmp = false; + if ((Params::instance().printTags_ & MetadataId::xmp) == MetadataId::xmp) { + const Exiv2::XmpData& xmpData = image->xmpData(); + for (auto&& md : xmpData) { + ret |= printMetadatum(md, image); + } + if (xmpData.empty()) + noXmp = true; + } + + // With -v, inform about the absence of any (requested) type of metadata + if (Params::instance().verbose_) { + if (noExif) + std::cerr << path_ << ": " << _("No Exif data found in the file") << "\n"; + if (noIptc) + std::cerr << path_ << ": " << _("No IPTC data found in the file") << "\n"; + if (noXmp) + std::cerr << path_ << ": " << _("No XMP data found in the file") << "\n"; + } + + // With -g or -K, return -3 if no matching tags were found + int rc = 0; + if ((!Params::instance().greps_.empty() || !Params::instance().keys_.empty()) && !ret) + rc = 1; + + return rc; +} // Print::printMetadata + +bool Print::grepTag(const std::string& key) { + bool result = Params::instance().greps_.empty(); + for (auto const& g : Params::instance().greps_) { + result = std::regex_search(key, g); + if (result) { + break; + } + } + return result; +} + +bool Print::keyTag(const std::string& key) { + bool result = Params::instance().keys_.empty(); + for (const auto& k : Params::instance().keys_) { + if (result) + break; + result = key == k; + } + return result; +} + +static void binaryOutput(const std::ostringstream& os) { + std::cout << os.str(); +} + +bool Print::printMetadatum(const Exiv2::Metadatum& md, const Exiv2::Image* pImage) { + if (!grepTag(md.key())) + return false; + if (!keyTag(md.key())) + return false; + + if (Params::instance().unknown_ && md.tagName().starts_with("0x")) { + return false; + } + + bool const manyFiles = Params::instance().files_.size() > 1; + if (manyFiles) { + std::ostringstream os; + std::ios::fmtflags f(os.flags()); + os << std::setfill(' ') << std::left << std::setw(20) << path_ << " "; + binaryOutput(os); + os.flags(f); + } + + bool first = true; + if (Params::instance().printItems_ & Params::prTag) { + first = false; + std::ostringstream os; + std::ios::fmtflags f(os.flags()); + os << "0x" << std::setw(4) << std::setfill('0') << std::right << std::hex << md.tag(); + binaryOutput(os); + os.flags(f); + } + if (Params::instance().printItems_ & Params::prSet) { + if (!first) + std::cout << " "; + first = false; + std::cout << "set"; + } + if (Params::instance().printItems_ & Params::prGroup) { + if (!first) + std::cout << " "; + first = false; + std::cout << std::setw(12) << std::setfill(' ') << std::left << md.groupName(); + } + if (Params::instance().printItems_ & Params::prKey) { + if (!first) + std::cout << " "; + first = false; + std::cout << std::setfill(' ') << std::left << std::setw(44) << md.key(); + } + if (Params::instance().printItems_ & Params::prName) { + if (!first) + std::cout << " "; + first = false; + std::cout << std::setw(27) << std::setfill(' ') << std::left << md.tagName(); + } + if (Params::instance().printItems_ & Params::prLabel) { + if (!first) + std::cout << " "; + first = false; + std::cout << std::setw(30) << std::setfill(' ') << std::left << md.tagLabel(); + } + if (Params::instance().printItems_ & Params::prDesc) { + if (!first) + std::cout << " "; + first = false; + std::cout << std::setw(30) << std::setfill(' ') << std::left << md.tagDesc(); + } + if (Params::instance().printItems_ & Params::prType) { + if (!first) + std::cout << " "; + first = false; + std::cout << std::setw(9) << std::setfill(' ') << std::left; + const char* tn = md.typeName(); + if (tn) { + std::cout << tn; + } else { + std::ostringstream os; + os << "0x" << std::setw(4) << std::setfill('0') << std::hex << md.typeId(); + std::cout << os.str(); + } + } + if (Params::instance().printItems_ & Params::prCount) { + if (!first) + std::cout << " "; + first = false; + std::cout << std::dec << std::setw(3) << std::setfill(' ') << std::right << md.count(); + } + if (Params::instance().printItems_ & Params::prSize) { + if (!first) + std::cout << " "; + first = false; + std::cout << std::dec << std::setw(3) << std::setfill(' ') << std::right << md.size(); + } + if (Params::instance().printItems_ & Params::prValue && md.size() > 0) { + if (!first) + std::cout << " "; + first = false; + std::ostringstream os; + std::ios::fmtflags f(os.flags()); + // #1114 - show negative values for SByte + if (md.typeId() == Exiv2::signedByte) { + for (size_t c = 0; c < md.value().count(); c++) { + const auto value = md.value().toInt64(c); + os << (c ? " " : "") << std::dec << (value < 128 ? value : value - 256); + } + } else { + os << std::dec << md.value(); + } + binaryOutput(os); + os.flags(f); + } + if (Params::instance().printItems_ & Params::prTrans) { + if (!first) + std::cout << " "; + first = false; + std::ostringstream os; + std::ios::fmtflags f(os.flags()); + os << std::dec << md.print(&pImage->exifData()); + binaryOutput(os); + os.flags(f); + } + if (Params::instance().printItems_ & Params::prHex) { + if (!first) + std::cout << '\n'; + if (md.size() > 0) { + Exiv2::DataBuf buf(md.size()); + md.copy(buf.data(), pImage->byteOrder()); + Exiv2::hexdump(std::cout, buf.c_data(), buf.size()); + } + } + std::cout << '\n'; + return true; +} // Print::printMetadatum + +int Print::printComment() { + if (!Exiv2::fileExists(path_)) { + std::cerr << path_ << ": " << _("Failed to open the file") << "\n"; + return -1; + } + + auto image = Exiv2::ImageFactory::open(path_); + image->readMetadata(); + if (Params::instance().verbose_) { + std::cout << _("JPEG comment") << ": "; + } + std::cout << image->comment() << '\n'; + return 0; +} // Print::printComment + +int Print::printPreviewList() { + if (!Exiv2::fileExists(path_)) { + std::cerr << path_ << ": " << _("Failed to open the file") << "\n"; + return -1; + } + + auto image = Exiv2::ImageFactory::open(path_); + image->readMetadata(); + bool const manyFiles = Params::instance().files_.size() > 1; + int cnt = 0; + Exiv2::PreviewManager pm(*image); + std::ostringstream os; + std::ios::fmtflags f(os.flags()); + Exiv2::PreviewPropertiesList list = pm.getPreviewProperties(); + for (const auto& pos : list) { + if (manyFiles) + os << std::setfill(' ') << std::left << std::setw(20) << path_ << " "; + + os << _("Preview") << " " << ++cnt << ": " << pos.mimeType_ << ", "; + + if (pos.width_ != 0 && pos.height_ != 0) + os << pos.width_ << "x" << pos.height_ << " " << _("pixels") << ", "; + + os << pos.size_ << " " << _("bytes") << "\n"; + } + binaryOutput(os); + os.flags(f); + return 0; +} // Print::printPreviewList + +Task::UniquePtr Print::clone() const { + return std::make_unique(*this); +} + +int Rename::run(const std::string& path) { + try { + if (!Exiv2::fileExists(path)) { + std::cerr << path << ": " << _("Failed to open the file") << "\n"; + return -1; + } + Timestamp ts; + if (Params::instance().preserve_) + ts.read(path); + + auto image = Exiv2::ImageFactory::open(path); + image->readMetadata(); + Exiv2::ExifData& exifData = image->exifData(); + if (exifData.empty()) { + std::cerr << path << ": " << _("No Exif data found in the file") << "\n"; + return -3; + } + auto md = exifData.findKey(Exiv2::ExifKey("Exif.Photo.DateTimeOriginal")); + if (md == exifData.end()) + md = exifData.findKey(Exiv2::ExifKey("Exif.Image.DateTime")); + if (md == exifData.end()) { + std::cerr << _("Neither tag") << " `Exif.Photo.DateTimeOriginal' " << _("nor") << " `Exif.Image.DateTime' " + << _("found in the file") << " " << path << "\n"; + return 1; + } + std::string v = md->toString(); + if (v.empty() || v.front() == ' ') { + std::cerr << _("Image file creation timestamp not set in the file") << " " << path << "\n"; + return 1; + } + std::tm tm; + if (str2Tm(v, &tm) != 0) { + std::cerr << _("Failed to parse timestamp") << " `" << v << "' " << _("in the file") << " " << path << "\n"; + return 1; + } + if (Params::instance().timestamp_ || Params::instance().timestampOnly_) { + ts.read(&tm); + } + int rc = 0; + std::string newPath = path; + if (Params::instance().timestampOnly_) { + if (Params::instance().verbose_) { + std::cout << _("Updating timestamp to") << " " << v << '\n'; + } + } else { + rc = renameFile(newPath, &tm, exifData); + if (rc == -1) + return 0; // skip + } + if (0 == rc && + (Params::instance().preserve_ || Params::instance().timestamp_ || Params::instance().timestampOnly_)) { + ts.touch(newPath); + } + return rc; + } catch (const Exiv2::Error& e) { + std::cerr << "Exiv2 exception in rename action for file " << path << ":\n" << e << "\n"; + return 1; + } +} + +Task::UniquePtr Rename::clone() const { + return std::make_unique(*this); +} + +int Erase::run(const std::string& path) { + try { + path_ = path; + + if (!Exiv2::fileExists(path_)) { + std::cerr << path_ << ": " << _("Failed to open the file") << "\n"; + return -1; + } + Timestamp ts; + if (Params::instance().preserve_) + ts.read(path); + + auto image = Exiv2::ImageFactory::open(path_); + image->readMetadata(); + // Thumbnail must be before Exif + int rc = 0; + if (Params::instance().target_ & Params::ctThumb) { + rc = eraseThumbnail(image.get()); + } + if (0 == rc && Params::instance().target_ & Params::ctExif) { + rc = eraseExifData(image.get()); + } + if (0 == rc && Params::instance().target_ & Params::ctIptc) { + rc = eraseIptcData(image.get()); + } + if (0 == rc && Params::instance().target_ & Params::ctComment) { + rc = eraseComment(image.get()); + } + if (0 == rc && Params::instance().target_ & Params::ctXmp) { + rc = eraseXmpData(image.get()); + } + if (0 == rc && Params::instance().target_ & Params::ctIccProfile) { + rc = eraseIccProfile(image.get()); + } + if (0 == rc && Params::instance().target_ & Params::ctIptcRaw) { + rc = printStructure(std::cout, Exiv2::kpsIptcErase, path_); + } + + if (0 == rc) { + image->writeMetadata(); + if (Params::instance().preserve_) + ts.touch(path); + } + + return rc; + } catch (const Exiv2::Error& e) { + std::cerr << "Exiv2 exception in erase action for file " << path << ":\n" << e << "\n"; + return 1; + } +} + +int Erase::eraseThumbnail(Exiv2::Image* image) { + Exiv2::ExifThumb exifThumb(image->exifData()); + std::string thumbExt = exifThumb.extension(); + if (thumbExt.empty()) { + return 0; + } + exifThumb.erase(); + if (Params::instance().verbose_) { + std::cout << _("Erasing thumbnail data") << '\n'; + } + return 0; +} + +int Erase::eraseExifData(Exiv2::Image* image) { + if (Params::instance().verbose_ && !image->exifData().empty()) { + std::cout << _("Erasing Exif data from the file") << '\n'; + } + image->clearExifData(); + return 0; +} + +int Erase::eraseIptcData(Exiv2::Image* image) { + if (Params::instance().verbose_ && !image->iptcData().empty()) { + std::cout << _("Erasing IPTC data from the file") << '\n'; + } + image->clearIptcData(); + return 0; +} + +int Erase::eraseComment(Exiv2::Image* image) { + if (Params::instance().verbose_ && !image->comment().empty()) { + std::cout << _("Erasing JPEG comment from the file") << '\n'; + } + image->clearComment(); + return 0; +} + +int Erase::eraseXmpData(Exiv2::Image* image) { + if (Params::instance().verbose_ && !image->xmpData().empty()) { + std::cout << _("Erasing XMP data from the file") << '\n'; + } + image->clearXmpData(); // Quick fix for bug #612 + image->clearXmpPacket(); + return 0; +} +int Erase::eraseIccProfile(Exiv2::Image* image) { + if (Params::instance().verbose_ && image->iccProfileDefined()) { + std::cout << _("Erasing ICC Profile data from the file") << '\n'; + } + image->clearIccProfile(); + return 0; +} + +Task::UniquePtr Erase::clone() const { + return std::make_unique(*this); +} + +int Extract::run(const std::string& path) { + try { + path_ = path; + int rc = 0; + + bool bStdout = (Params::instance().target_ & Params::ctStdInOut) != 0; + if (bStdout) { + _setmode(_fileno(stdout), _O_BINARY); + } + + if (Params::instance().target_ & Params::ctThumb) { + rc = writeThumbnail(); + } + if (!rc && Params::instance().target_ & Params::ctPreview) { + rc = writePreviews(); + } + if (!rc && Params::instance().target_ & Params::ctXmpSidecar) { + std::string xmpPath = bStdout ? "-" : newFilePath(path_, ".xmp"); + if (dontOverwrite(xmpPath)) + return 0; + rc = metacopy(path_, xmpPath, Exiv2::ImageType::xmp, false); + } + if (!rc && Params::instance().target_ & Params::ctIccProfile) { + std::string iccPath = bStdout ? "-" : newFilePath(path_, ".icc"); + rc = writeIccProfile(iccPath); + } + if (!rc && !(Params::instance().target_ & Params::ctXmpSidecar) && + !(Params::instance().target_ & Params::ctThumb) && !(Params::instance().target_ & Params::ctPreview) && + !(Params::instance().target_ & Params::ctIccProfile)) { + std::string exvPath = bStdout ? "-" : newFilePath(path_, ".exv"); + if (dontOverwrite(exvPath)) + return 0; + rc = metacopy(path_, exvPath, Exiv2::ImageType::exv, false); + } + return rc; + } catch (const Exiv2::Error& e) { + std::cerr << "Exiv2 exception in extract action for file " << path << ":\n" << e << "\n"; + return 1; + } +} + +int Extract::writeThumbnail() const { + if (!Exiv2::fileExists(path_)) { + std::cerr << path_ << ": " << _("Failed to open the file") << "\n"; + return -1; + } + auto image = Exiv2::ImageFactory::open(path_); + image->readMetadata(); + Exiv2::ExifData& exifData = image->exifData(); + if (exifData.empty()) { + std::cerr << path_ << ": " << _("No Exif data found in the file") << "\n"; + return -3; + } + int rc = 0; + Exiv2::ExifThumb exifThumb(exifData); + std::string thumbExt = exifThumb.extension(); + if (thumbExt.empty()) { + std::cerr << path_ << ": " << _("Image does not contain an Exif thumbnail") << "\n"; + } else { + if ((Params::instance().target_ & Params::ctStdInOut) != 0) { + Exiv2::DataBuf buf = exifThumb.copy(); + std::cout.write(buf.c_str(), buf.size()); + return 0; + } + + std::string thumb = newFilePath(path_, "-thumb"); + std::string thumbPath = thumb + thumbExt; + if (dontOverwrite(thumbPath)) + return 0; + if (Params::instance().verbose_) { + Exiv2::DataBuf buf = exifThumb.copy(); + if (!buf.empty()) { + std::cout << _("Writing thumbnail") << " (" << exifThumb.mimeType() << ", " << buf.size() << " " << _("Bytes") + << ") " << _("to file") << " " << thumbPath << '\n'; + } + } + rc = static_cast(exifThumb.writeFile(thumb)); + if (rc == 0) { + std::cerr << path_ << ": " << _("Exif data doesn't contain a thumbnail") << "\n"; + } + } + return rc; +} // Extract::writeThumbnail + +int Extract::writePreviews() const { + if (!Exiv2::fileExists(path_)) { + std::cerr << path_ << ": " << _("Failed to open the file") << "\n"; + return -1; + } + + auto image = Exiv2::ImageFactory::open(path_); + image->readMetadata(); + + Exiv2::PreviewManager pvMgr(*image); + Exiv2::PreviewPropertiesList pvList = pvMgr.getPreviewProperties(); + + const Params::PreviewNumbers& numbers = Params::instance().previewNumbers_; + for (auto number : numbers) { + auto num = static_cast(number); + if (num == 0) { + // Write all previews + for (num = 0; num < pvList.size(); ++num) { + writePreviewFile(pvMgr.getPreviewImage(pvList[num]), num + 1); + } + break; + } + num--; + if (num >= pvList.size()) { + std::cerr << path_ << ": " << _("Image does not have preview") << " " << num + 1 << "\n"; + continue; + } + writePreviewFile(pvMgr.getPreviewImage(pvList[num]), num + 1); + } + return 0; +} // Extract::writePreviews + +int Extract::writeIccProfile(const std::string& target) const { + int rc = 0; + if (!Exiv2::fileExists(path_)) { + std::cerr << path_ << ": " << _("Failed to open the file") << "\n"; + rc = -1; + } + + bool bStdout = target == "-"; + + if (rc == 0) { + auto image = Exiv2::ImageFactory::open(path_); + image->readMetadata(); + if (!image->iccProfileDefined()) { + std::cerr << _("No embedded iccProfile: ") << path_ << '\n'; + rc = -2; + } else { + if (bStdout) { // -eC- + std::cout.write(image->iccProfile().c_str(), image->iccProfile().size()); + } else { + if (Params::instance().verbose_) { + std::cout << _("Writing iccProfile: ") << target << '\n'; + } + Exiv2::FileIo iccFile(target); + iccFile.open("wb"); + iccFile.write(image->iccProfile().c_data(), image->iccProfile().size()); + iccFile.close(); + } + } + } + return rc; +} // Extract::writeIccProfile + +void Extract::writePreviewFile(const Exiv2::PreviewImage& pvImg, size_t num) const { + std::string pvFile = newFilePath(path_, "-preview") + std::to_string(num); + std::string pvPath = pvFile + pvImg.extension(); + if (dontOverwrite(pvPath)) + return; + if (Params::instance().verbose_) { + std::cout << _("Writing preview") << " " << num << " (" << pvImg.mimeType() << ", "; + if (pvImg.width() != 0 && pvImg.height() != 0) { + std::cout << pvImg.width() << "x" << pvImg.height() << " " << _("pixels") << ", "; + } + std::cout << pvImg.size() << " " << _("bytes") << ") " << _("to file") << " " << pvPath << '\n'; + } + auto rc = pvImg.writeFile(pvFile); + if (rc == 0) { + std::cerr << path_ << ": " << _("Image does not have preview") << " " << num << "\n"; + } +} + +Task::UniquePtr Extract::clone() const { + return std::make_unique(*this); +} + +int Insert::run(const std::string& path) try { + // -i{tgt}- reading from stdin? + bool bStdin = (Params::instance().target_ & Params::ctStdInOut) != 0; + + if (!Exiv2::fileExists(path)) { + std::cerr << path << ": " << _("Failed to open the file") << "\n"; + return -1; + } + + int rc = 0; + Timestamp ts; + if (Params::instance().preserve_) + ts.read(path); + + if (Params::instance().target_ & Params::ctThumb) { + rc = insertThumbnail(path); + } + + if (rc == 0 && !(Params::instance().target_ & Params::ctXmpRaw) && + (Params::instance().target_ & Params::ctExif || Params::instance().target_ & Params::ctIptc || + Params::instance().target_ & Params::ctComment || Params::instance().target_ & Params::ctXmp)) { + std::string suffix = Params::instance().suffix_; + if (suffix.empty()) + suffix = ".exv"; + if (Params::instance().target_ & Params::ctXmpSidecar) + suffix = ".xmp"; + std::string exvPath = bStdin ? "-" : newFilePath(path, suffix); + rc = metacopy(exvPath, path, Exiv2::ImageType::none, true); + } + + if (0 == rc && (Params::instance().target_ & (Params::ctXmpSidecar | Params::ctXmpRaw))) { + std::string xmpPath = bStdin ? "-" : newFilePath(path, ".xmp"); + rc = insertXmpPacket(path, xmpPath); + } + + if (0 == rc && Params::instance().target_ & Params::ctIccProfile) { + std::string iccPath = bStdin ? "-" : newFilePath(path, ".icc"); + rc = insertIccProfile(path, iccPath); + } + + if (Params::instance().preserve_) + ts.touch(path); + return rc; +} catch (const Exiv2::Error& e) { + std::cerr << "Exiv2 exception in insert action for file " << path << ":\n" << e << "\n"; + return 1; +} // Insert::run + +int Insert::insertXmpPacket(const std::string& path, const std::string& xmpPath) { + int rc = 0; + bool bStdin = xmpPath == "-"; + if (bStdin) { + Exiv2::DataBuf xmpBlob; + Params::instance().getStdin(xmpBlob); + rc = insertXmpPacket(path, xmpBlob, true); + } else { + if (!Exiv2::fileExists(xmpPath)) { + std::cerr << xmpPath << ": " << _("Failed to open the file") << "\n"; + rc = -1; + } + if (rc == 0 && !Exiv2::fileExists(path)) { + std::cerr << path << ": " << _("Failed to open the file") << "\n"; + rc = -1; + } + if (rc == 0) { + Exiv2::DataBuf xmpBlob = Exiv2::readFile(xmpPath); + rc = insertXmpPacket(path, xmpBlob); + } + } + return rc; + +} // Insert::insertXmpPacket + +int Insert::insertXmpPacket(const std::string& path, const Exiv2::DataBuf& xmpBlob, bool usePacket) { + std::string xmpPacket(xmpBlob.begin(), xmpBlob.end()); + auto image = Exiv2::ImageFactory::open(path); + image->readMetadata(); + image->clearXmpData(); + image->setXmpPacket(xmpPacket); + image->writeXmpFromPacket(usePacket); + image->writeMetadata(); + + return 0; +} + +int Insert::insertIccProfile(const std::string& path, const std::string& iccPath) { + int rc = 0; + // for path "foo.XXX", do a binary copy of "foo.icc" + std::string iccProfilePath = newFilePath(path, ".icc"); + if (iccPath == "-") { + Exiv2::DataBuf iccProfile; + Params::instance().getStdin(iccProfile); + rc = insertIccProfile(path, std::move(iccProfile)); + } else { + if (!Exiv2::fileExists(iccProfilePath)) { + std::cerr << iccProfilePath << ": " << _("Failed to open the file") << "\n"; + rc = -1; + } else { + Exiv2::DataBuf iccProfile = Exiv2::readFile(iccPath); + rc = insertIccProfile(path, std::move(iccProfile)); + } + } + return rc; +} // Insert::insertIccProfile + +int Insert::insertIccProfile(const std::string& path, Exiv2::DataBuf&& iccProfileBlob) { + int rc = 0; + // test path exists + if (!Exiv2::fileExists(path)) { + std::cerr << path << ": " << _("Failed to open the file") << "\n"; + rc = -1; + } + + // read in the metadata + if (rc == 0) { + auto image = Exiv2::ImageFactory::open(path); + image->readMetadata(); + // clear existing profile, assign the blob and rewrite image + image->clearIccProfile(); + if (!iccProfileBlob.empty()) { + image->setIccProfile(std::move(iccProfileBlob)); + } + image->writeMetadata(); + } + + return rc; +} // Insert::insertIccProfile + +int Insert::insertThumbnail(const std::string& path) { + std::string thumbPath = newFilePath(path, "-thumb.jpg"); + if (!Exiv2::fileExists(thumbPath)) { + std::cerr << thumbPath << ": " << _("Failed to open the file") << "\n"; + return -1; + } + if (!Exiv2::fileExists(path)) { + std::cerr << path << ": " << _("Failed to open the file") << "\n"; + return -1; + } + auto image = Exiv2::ImageFactory::open(path); + image->readMetadata(); + Exiv2::ExifThumb exifThumb(image->exifData()); + exifThumb.setJpegThumbnail(thumbPath); + image->writeMetadata(); + + return 0; +} // Insert::insertThumbnail + +Task::UniquePtr Insert::clone() const { + return std::make_unique(*this); +} + +int Modify::run(const std::string& path) { + try { + if (!Exiv2::fileExists(path)) { + std::cerr << path << ": " << _("Failed to open the file") << "\n"; + return -1; + } + Timestamp ts; + if (Params::instance().preserve_) + ts.read(path); + + auto image = Exiv2::ImageFactory::open(path); + image->readMetadata(); + + int rc = applyCommands(image.get()); + + // Save both exif and iptc metadata + image->writeMetadata(); + + if (Params::instance().preserve_) + ts.touch(path); + + return rc; + } catch (const Exiv2::Error& e) { + std::cerr << "Exiv2 exception in modify action for file " << path << ":\n" << e << "\n"; + return 1; + } +} // Modify::run + +int Modify::applyCommands(Exiv2::Image* pImage) { + if (!Params::instance().jpegComment_.empty()) { + // If modify is used when extracting to stdout then ignore verbose + if (Params::instance().verbose_ && + !(Params::instance().action_ & Action::extract && Params::instance().target_ & Params::ctStdInOut)) { + std::cout << _("Setting JPEG comment") << " '" << Params::instance().jpegComment_ << "'" << '\n'; + } + pImage->setComment(Params::instance().jpegComment_); + } + + // loop through command table and apply each command + const ModifyCmds& modifyCmds = Params::instance().modifyCmds_; + int rc = 0; + int ret = 0; + for (const auto& cmd : modifyCmds) { + switch (cmd.cmdId_) { + case CmdId::add: + ret = addMetadatum(pImage, cmd); + if (rc == 0) + rc = ret; + break; + case CmdId::set: + ret = setMetadatum(pImage, cmd); + if (rc == 0) + rc = ret; + break; + case CmdId::del: + delMetadatum(pImage, cmd); + break; + case CmdId::reg: + regNamespace(cmd); + break; + case CmdId::invalid: + break; + } + } + return rc; +} // Modify::applyCommands + +int Modify::addMetadatum(Exiv2::Image* pImage, const ModifyCmd& modifyCmd) { + // If modify is used when extracting to stdout then ignore verbose + if (Params::instance().verbose_ && + !(Params::instance().action_ & Action::extract && Params::instance().target_ & Params::ctStdInOut)) { + std::cout << _("Add") << " " << modifyCmd.key_ << " \"" << modifyCmd.value_ << "\" (" + << Exiv2::TypeInfo::typeName(modifyCmd.typeId_) << ")" << '\n'; + } + Exiv2::ExifData& exifData = pImage->exifData(); + Exiv2::IptcData& iptcData = pImage->iptcData(); + Exiv2::XmpData& xmpData = pImage->xmpData(); + auto value = Exiv2::Value::create(modifyCmd.typeId_); + int rc = value->read(modifyCmd.value_); + if (0 == rc) { + if (modifyCmd.metadataId_ == MetadataId::exif) { + exifData.add(Exiv2::ExifKey(modifyCmd.key_), value.get()); + } + if (modifyCmd.metadataId_ == MetadataId::iptc) { + iptcData.add(Exiv2::IptcKey(modifyCmd.key_), value.get()); + } + if (modifyCmd.metadataId_ == MetadataId::xmp) { + xmpData.add(Exiv2::XmpKey(modifyCmd.key_), value.get()); + } + } else { + std::cerr << _("Warning") << ": " << modifyCmd.key_ << ": " << _("Failed to read") << " " + << Exiv2::TypeInfo::typeName(value->typeId()) << " " << _("value") << " \"" << modifyCmd.value_ << "\"\n"; + } + return rc; +} + +// This function looks rather complex because we try to avoid adding an +// empty metadatum if reading the value fails +int Modify::setMetadatum(Exiv2::Image* pImage, const ModifyCmd& modifyCmd) { + // If modify is used when extracting to stdout then ignore verbose + if (Params::instance().verbose_ && + !(Params::instance().action_ & Action::extract && Params::instance().target_ & Params::ctStdInOut)) { + std::cout << _("Set") << " " << modifyCmd.key_ << " \"" << modifyCmd.value_ << "\" (" + << Exiv2::TypeInfo::typeName(modifyCmd.typeId_) << ")" << '\n'; + } + Exiv2::ExifData& exifData = pImage->exifData(); + Exiv2::IptcData& iptcData = pImage->iptcData(); + Exiv2::XmpData& xmpData = pImage->xmpData(); + Exiv2::Metadatum* metadatum = nullptr; + if (modifyCmd.metadataId_ == MetadataId::exif) { + auto pos = exifData.findKey(Exiv2::ExifKey(modifyCmd.key_)); + if (pos != exifData.end()) { + metadatum = &(*pos); + } + } + if (modifyCmd.metadataId_ == MetadataId::iptc) { + auto pos = iptcData.findKey(Exiv2::IptcKey(modifyCmd.key_)); + if (pos != iptcData.end()) { + metadatum = &(*pos); + } + } + if (modifyCmd.metadataId_ == MetadataId::xmp) { + auto pos = xmpData.findKey(Exiv2::XmpKey(modifyCmd.key_)); + if (pos != xmpData.end()) { + metadatum = &(*pos); + } + } + // If a type was explicitly requested, use it; else + // use the current type of the metadatum, if any; + // or the default type + Exiv2::Value::UniquePtr value; + if (metadatum) { + value = metadatum->getValue(); + } + if (!value || (modifyCmd.explicitType_ && modifyCmd.typeId_ != value->typeId())) { + value = Exiv2::Value::create(modifyCmd.typeId_); + } + int rc = value->read(modifyCmd.value_); + if (0 == rc) { + if (metadatum) { + metadatum->setValue(value.get()); + } else { + if (modifyCmd.metadataId_ == MetadataId::exif) { + exifData.add(Exiv2::ExifKey(modifyCmd.key_), value.get()); + } + if (modifyCmd.metadataId_ == MetadataId::iptc) { + iptcData.add(Exiv2::IptcKey(modifyCmd.key_), value.get()); + } + if (modifyCmd.metadataId_ == MetadataId::xmp) { + xmpData.add(Exiv2::XmpKey(modifyCmd.key_), value.get()); + } + } + } else { + std::cerr << _("Warning") << ": " << modifyCmd.key_ << ": " << _("Failed to read") << " " + << Exiv2::TypeInfo::typeName(value->typeId()) << " " << _("value") << " \"" << modifyCmd.value_ << "\"\n"; + } + return rc; +} + +void Modify::delMetadatum(Exiv2::Image* pImage, const ModifyCmd& modifyCmd) { + // If modify is used when extracting to stdout then ignore verbose + if (Params::instance().verbose_ && + !(Params::instance().action_ & Action::extract && Params::instance().target_ & Params::ctStdInOut)) { + std::cout << _("Del") << " " << modifyCmd.key_ << '\n'; + } + + Exiv2::ExifData& exifData = pImage->exifData(); + Exiv2::IptcData& iptcData = pImage->iptcData(); + Exiv2::XmpData& xmpData = pImage->xmpData(); + if (modifyCmd.metadataId_ == MetadataId::exif) { + Exiv2::ExifData::iterator pos; + const Exiv2::ExifKey exifKey(modifyCmd.key_); + while ((pos = exifData.findKey(exifKey)) != exifData.end()) { + exifData.erase(pos); + } + } + if (modifyCmd.metadataId_ == MetadataId::iptc) { + Exiv2::IptcData::iterator pos; + const Exiv2::IptcKey iptcKey(modifyCmd.key_); + while ((pos = iptcData.findKey(iptcKey)) != iptcData.end()) { + iptcData.erase(pos); + } + } + if (modifyCmd.metadataId_ == MetadataId::xmp) { + const Exiv2::XmpKey xmpKey(modifyCmd.key_); + auto pos = xmpData.findKey(xmpKey); + if (pos != xmpData.end()) { + xmpData.eraseFamily(pos); + } + } +} + +void Modify::regNamespace(const ModifyCmd& modifyCmd) { + // If modify is used when extracting to stdout then ignore verbose + if (Params::instance().verbose_ && + !(Params::instance().action_ & Action::extract && Params::instance().target_ & Params::ctStdInOut)) { + std::cout << _("Reg ") << modifyCmd.key_ << "=\"" << modifyCmd.value_ << "\"" << '\n'; + } + Exiv2::XmpProperties::registerNs(modifyCmd.value_, modifyCmd.key_); +} + +Task::UniquePtr Modify::clone() const { + return std::make_unique(*this); +} + +int Adjust::run(const std::string& path) try { + adjustment_ = Params::instance().adjustment_; + yearAdjustment_ = Params::instance().yodAdjust_[Params::yodYear].adjustment_; + monthAdjustment_ = Params::instance().yodAdjust_[Params::yodMonth].adjustment_; + dayAdjustment_ = Params::instance().yodAdjust_[Params::yodDay].adjustment_; + + if (!Exiv2::fileExists(path)) { + std::cerr << path << ": " << _("Failed to open the file") << "\n"; + return -1; + } + Timestamp ts; + if (Params::instance().preserve_) + ts.read(path); + + auto image = Exiv2::ImageFactory::open(path); + image->readMetadata(); + Exiv2::ExifData& exifData = image->exifData(); + if (exifData.empty()) { + std::cerr << path << ": " << _("No Exif data found in the file") << "\n"; + return -3; + } + int rc = adjustDateTime(exifData, "Exif.Image.DateTime", path); + rc += adjustDateTime(exifData, "Exif.Photo.DateTimeOriginal", path); + rc += adjustDateTime(exifData, "Exif.Image.DateTimeOriginal", path); + rc += adjustDateTime(exifData, "Exif.Photo.DateTimeDigitized", path); + + if (rc == 0) { + image->writeMetadata(); + if (Params::instance().preserve_) + ts.touch(path); + } + return rc ? 1 : 0; +} catch (const Exiv2::Error& e) { + std::cerr << "Exiv2 exception in adjust action for file " << path << ":\n" << e << "\n"; + return 1; +} // Adjust::run + +Task::UniquePtr Adjust::clone() const { + return std::make_unique(*this); +} + +int Adjust::adjustDateTime(Exiv2::ExifData& exifData, const std::string& key, const std::string& path) const { + Exiv2::ExifKey ek(key); + auto md = exifData.findKey(ek); + if (md == exifData.end()) { + // Key not found. That's ok, we do nothing. + return 0; + } + std::string timeStr = md->toString(); + if (timeStr.empty() || timeStr[0] == ' ') { + std::cerr << path << ": " << _("Timestamp of metadatum with key") << " `" << ek << "' " << _("not set") << "\n"; + return 1; + } + if (Params::instance().verbose_) { + bool comma = false; + std::cout << _("Adjusting") << " `" << ek << "' " << _("by"); + if (yearAdjustment_ != 0) { + std::cout << (yearAdjustment_ < 0 ? " " : " +") << yearAdjustment_ << " "; + if (yearAdjustment_ < -1 || yearAdjustment_ > 1) { + std::cout << _("years"); + } else { + std::cout << _("year"); + } + comma = true; + } + if (monthAdjustment_ != 0) { + if (comma) + std::cout << ","; + std::cout << (monthAdjustment_ < 0 ? " " : " +") << monthAdjustment_ << " "; + if (monthAdjustment_ < -1 || monthAdjustment_ > 1) { + std::cout << _("months"); + } else { + std::cout << _("month"); + } + comma = true; + } + if (dayAdjustment_ != 0) { + if (comma) + std::cout << ","; + std::cout << (dayAdjustment_ < 0 ? " " : " +") << dayAdjustment_ << " "; + if (dayAdjustment_ < -1 || dayAdjustment_ > 1) { + std::cout << _("days"); + } else { + std::cout << _("day"); + } + comma = true; + } + if (adjustment_ != 0) { + if (comma) + std::cout << ","; + std::cout << " " << adjustment_ << _("s"); + } + } + std::tm tm; + if (str2Tm(timeStr, &tm) != 0) { + if (Params::instance().verbose_) + std::cout << '\n'; + std::cerr << path << ": " << _("Failed to parse timestamp") << " `" << timeStr << "'\n"; + return 1; + } + + // bounds checking for yearAdjustment_ + Exiv2::Internal::enforce(yearAdjustment_ >= std::numeric_limits::min(), + "year adjustment too low"); + Exiv2::Internal::enforce(yearAdjustment_ <= std::numeric_limits::max(), + "year adjustment too high"); + const auto yearAdjustment = static_cast(yearAdjustment_); + + // bounds checking for monthAdjustment_ + Exiv2::Internal::enforce(monthAdjustment_ >= std::numeric_limits::min(), + "month adjustment too low"); + Exiv2::Internal::enforce(monthAdjustment_ <= std::numeric_limits::max(), + "month adjustment too high"); + const auto monthAdjustment = static_cast(monthAdjustment_); + + // bounds checking for dayAdjustment_ + static constexpr time_t secondsInDay = 24 * 60 * 60; + Exiv2::Internal::enforce(dayAdjustment_ >= std::numeric_limits::min() / secondsInDay, + "day adjustment too low"); + Exiv2::Internal::enforce(dayAdjustment_ <= std::numeric_limits::max() / secondsInDay, + "day adjustment too high"); + const auto dayAdjustment = static_cast(dayAdjustment_); + + // bounds checking for adjustment_ + Exiv2::Internal::enforce(adjustment_ >= std::numeric_limits::min(), + "seconds adjustment too low"); + Exiv2::Internal::enforce(adjustment_ <= std::numeric_limits::max(), + "seconds adjustment too high"); + const auto adjustment = static_cast(adjustment_); + + const auto monOverflow = Safe::add(tm.tm_mon, monthAdjustment) / 12; + tm.tm_mon = Safe::add(tm.tm_mon, monthAdjustment) % 12; + tm.tm_year = Safe::add(tm.tm_year, Safe::add(yearAdjustment, monOverflow)); + // Let's not create files with non-4-digit years, we can't read them. + if (tm.tm_year > 9999 - 1900 || tm.tm_year < 1000 - 1900) { + if (Params::instance().verbose_) + std::cout << '\n'; + std::cerr << path << ": " << _("Can't adjust timestamp by") << " " << yearAdjustment + monOverflow << " " + << _("years") << "\n"; + return 1; + } + time_t time = mktime(&tm); + time = Safe::add(time, Safe::add(adjustment, dayAdjustment * secondsInDay)); + timeStr = time2Str(time); + if (Params::instance().verbose_) { + std::cout << " " << _("to") << " " << timeStr << '\n'; + } + md->setValue(timeStr); + return 0; +} // Adjust::adjustDateTime + +int FixIso::run(const std::string& path) { + try { + if (!Exiv2::fileExists(path)) { + std::cerr << path << ": " << _("Failed to open the file") << "\n"; + return -1; + } + Timestamp ts; + if (Params::instance().preserve_) + ts.read(path); + + auto image = Exiv2::ImageFactory::open(path); + image->readMetadata(); + Exiv2::ExifData& exifData = image->exifData(); + if (exifData.empty()) { + std::cerr << path << ": " << _("No Exif data found in the file") << "\n"; + return -3; + } + auto md = Exiv2::isoSpeed(exifData); + if (md != exifData.end()) { + if (md->key() == "Exif.Photo.ISOSpeedRatings") { + if (Params::instance().verbose_) { + std::cout << _("Standard Exif ISO tag exists; not modified") << "\n"; + } + return 0; + } + // Copy the proprietary tag to the standard place + std::ostringstream os; + md->write(os, &exifData); + if (Params::instance().verbose_) { + std::cout << _("Setting Exif ISO value to") << " " << os.str() << "\n"; + } + exifData["Exif.Photo.ISOSpeedRatings"] = os.str(); + } + image->writeMetadata(); + if (Params::instance().preserve_) + ts.touch(path); + + return 0; + } catch (const Exiv2::Error& e) { + std::cerr << "Exiv2 exception in fixiso action for file " << path << ":\n" << e << "\n"; + return 1; + } +} // FixIso::run + +Task::UniquePtr FixIso::clone() const { + return std::make_unique(*this); +} + +int FixCom::run(const std::string& path) { + try { + if (!Exiv2::fileExists(path)) { + std::cerr << path << ": " << _("Failed to open the file") << "\n"; + return -1; + } + Timestamp ts; + if (Params::instance().preserve_) + ts.read(path); + + auto image = Exiv2::ImageFactory::open(path); + image->readMetadata(); + Exiv2::ExifData& exifData = image->exifData(); + if (exifData.empty()) { + std::cerr << path << ": " << _("No Exif data found in the file") << "\n"; + return -3; + } + auto pos = exifData.findKey(Exiv2::ExifKey("Exif.Photo.UserComment")); + if (pos == exifData.end()) { + if (Params::instance().verbose_) { + std::cout << _("No Exif user comment found") << "\n"; + } + return 0; + } + Exiv2::Value::UniquePtr v = pos->getValue(); + const auto pcv = dynamic_cast(v.get()); + if (!pcv) { + if (Params::instance().verbose_) { + std::cout << _("Found Exif user comment with unexpected value type") << "\n"; + } + return 0; + } + Exiv2::CommentValue::CharsetId csId = pcv->charsetId(); + if (csId != Exiv2::CommentValue::unicode) { + if (Params::instance().verbose_) { + std::cout << _("No Exif UNICODE user comment found") << "\n"; + } + return 0; + } + std::string comment = pcv->comment(Params::instance().charset_.c_str()); + if (Params::instance().verbose_) { + std::cout << _("Setting Exif UNICODE user comment to") << " \"" << comment << "\"\n"; + } + comment = std::string("charset=\"") + Exiv2::CommentValue::CharsetInfo::name(csId) + "\" " + comment; + // Remove BOM and convert value from source charset to UCS-2, but keep byte order + pos->setValue(comment); + image->writeMetadata(); + if (Params::instance().preserve_) + ts.touch(path); + + return 0; + } catch (const Exiv2::Error& e) { + std::cerr << "Exiv2 exception in fixcom action for file " << path << ":\n" << e << "\n"; + return 1; + } +} // FixCom::run + +Task::UniquePtr FixCom::clone() const { + return std::make_unique(*this); +} + +} // namespace Action + +// ***************************************************************************** +// local definitions +namespace { +//! @cond IGNORE +int Timestamp::read(const std::string& path) { + struct stat buf; + int rc = stat(path.c_str(), &buf); + if (0 == rc) { + actime_ = buf.st_atime; + modtime_ = buf.st_mtime; + } + return rc; +} + +int Timestamp::read(tm* tm) { + int rc = 1; + time_t t = mktime(tm); // interpret tm according to current timezone settings + if (t != time_t{-1}) { + rc = 0; + actime_ = t; + modtime_ = t; + } + return rc; +} + +int Timestamp::touch(const std::string& path) const { + if (0 == actime_) + return 1; + utimbuf buf; + buf.actime = actime_; + buf.modtime = modtime_; + return utime(path.c_str(), &buf); +} +//! @endcond + +int str2Tm(const std::string& timeStr, tm* tm) { + if (timeStr.empty() || timeStr.front() == ' ') + return 1; + if (timeStr.length() < 19) + return 2; + if ((timeStr[4] != ':' && timeStr[4] != '-') || (timeStr[7] != ':' && timeStr[7] != '-') || timeStr[10] != ' ' || + timeStr[13] != ':' || timeStr[16] != ':') + return 3; + if (!tm) + return 4; + std::memset(tm, 0x0, sizeof(*tm)); + tm->tm_isdst = -1; + + int64_t tmp = 0; + if (!Util::strtol(timeStr.substr(0, 4).c_str(), tmp)) + return 5; + // tmp is a 4-digit number so this cast cannot overflow + tm->tm_year = static_casttm_year)>(tmp - 1900); + if (!Util::strtol(timeStr.substr(5, 2).c_str(), tmp)) + return 6; + // tmp is a 2-digit number so this cast cannot overflow + tm->tm_mon = static_casttm_mon)>(tmp - 1); + if (!Util::strtol(timeStr.substr(8, 2).c_str(), tmp)) + return 7; + // tmp is a 2-digit number so this cast cannot overflow + tm->tm_mday = static_casttm_mday)>(tmp); + if (!Util::strtol(timeStr.substr(11, 2).c_str(), tmp)) + return 8; + // tmp is a 2-digit number so this cast cannot overflow + tm->tm_hour = static_casttm_hour)>(tmp); + if (!Util::strtol(timeStr.substr(14, 2).c_str(), tmp)) + return 9; + // tmp is a 2-digit number so this cast cannot overflow + tm->tm_min = static_casttm_min)>(tmp); + if (!Util::strtol(timeStr.substr(17, 2).c_str(), tmp)) + return 10; + // tmp is a 2-digit number so this cast cannot overflow + tm->tm_sec = static_casttm_sec)>(tmp); + + // Conversions to set remaining fields of the tm structure + if (mktime(tm) == time_t{-1}) + return 11; + + return 0; +} // str2Tm + +std::string time2Str(time_t time) { + std::tm r; +#ifdef _WIN32 + auto tm = localtime_s(&r, &time) ? nullptr : &r; +#else + auto tm = localtime_r(&time, &r); +#endif + if (!tm) + return ""; + + const size_t m = 20; + char s[m]; + std::strftime(s, m, "%Y:%m:%d %T", tm); + return s; +} // time2Str + +std::string temporaryPath() { + static int count = 0; + auto guard = std::scoped_lock(cs); + +#ifdef _WIN32 + DWORD pid = ::GetCurrentProcessId(); +#else + pid_t pid = ::getpid(); +#endif + /// \todo check if we can use std::tmpnam + auto p = fs::temp_directory_path() / (Exiv2::toString(pid) + "_" + std::to_string(count)); + if (fs::exists(p)) { + fs::remove(p); + } + + return p.string(); +} + +int metacopy(const std::string& source, const std::string& tgt, Exiv2::ImageType targetType, bool preserve) { +#ifdef EXIV2_DEBUG_MESSAGES + std::cerr << "actions.cpp::metacopy" + << " source = " << source << " target = " << tgt << '\n'; +#endif + + // read the source metadata + int rc = -1; + if (!Exiv2::fileExists(source)) { + std::cerr << source << ": " << _("Failed to open the file") << "\n"; + return rc; + } + + bool bStdin = source == "-"; + bool bStdout = tgt == "-"; + + Exiv2::DataBuf stdIn; + Exiv2::Image::UniquePtr sourceImage; + if (bStdin) { + Params::instance().getStdin(stdIn); + auto ioStdin = std::make_unique(stdIn.c_data(), stdIn.size()); + sourceImage = Exiv2::ImageFactory::open(std::move(ioStdin)); + } else { + sourceImage = Exiv2::ImageFactory::open(source); + } + + sourceImage->readMetadata(); + + // Apply any modification commands to the source image on-the-fly + Action::Modify::applyCommands(sourceImage.get()); + + // Open or create the target file + std::string target(bStdout ? temporaryPath() : tgt); + + std::unique_ptr targetImage; + if (Exiv2::fileExists(target)) { + targetImage = Exiv2::ImageFactory::open(target); + targetImage->readMetadata(); + } else { + targetImage = Exiv2::ImageFactory::create(targetType, target); + } + + // Copy each type of metadata + if (Params::instance().target_ & Params::ctExif && !sourceImage->exifData().empty()) { + if (Params::instance().verbose_ && !bStdout) { + std::cout << _("Writing Exif data from") << " " << source << " " << _("to") << " " << target << '\n'; + } + if (preserve) { + for (const auto& exif : sourceImage->exifData()) { + targetImage->exifData()[exif.key()] = exif.value(); + } + } else { + targetImage->setExifData(sourceImage->exifData()); + } + } + if (Params::instance().target_ & Params::ctIptc && !sourceImage->iptcData().empty()) { + if (Params::instance().verbose_ && !bStdout) { + std::cout << _("Writing IPTC data from") << " " << source << " " << _("to") << " " << target << '\n'; + } + if (preserve) { + for (const auto& iptc : sourceImage->iptcData()) { + targetImage->iptcData()[iptc.key()] = iptc.value(); + } + } else { + targetImage->setIptcData(sourceImage->iptcData()); + } + } + if (Params::instance().target_ & (Params::ctXmp | Params::ctXmpRaw) && !sourceImage->xmpData().empty()) { + if (Params::instance().verbose_ && !bStdout) { + std::cout << _("Writing XMP data from") << " " << source << " " << _("to") << " " << target << '\n'; + } + + // #1148 use Raw XMP packet if there are no XMP modification commands + Params::CommonTarget tRawSidecar = Params::ctXmpSidecar | Params::ctXmpRaw; // option -eXX + if (Params::instance().modifyCmds_.empty() && (Params::instance().target_ & tRawSidecar) == tRawSidecar) { + // std::cout << "short cut" << '\n'; + // http://www.cplusplus.com/doc/tutorial/files/ + std::ofstream os; + os.open(target.c_str()); + sourceImage->printStructure(os, Exiv2::kpsXMP); + os.close(); + rc = 0; + } else if (preserve) { + for (const auto& xmp : sourceImage->xmpData()) { + targetImage->xmpData()[xmp.key()] = xmp.value(); + } + } else { + // std::cout << "long cut" << '\n'; + targetImage->setXmpData(sourceImage->xmpData()); + } + } + if (Params::instance().target_ & Params::ctComment && !sourceImage->comment().empty()) { + if (Params::instance().verbose_ && !bStdout) { + std::cout << _("Writing JPEG comment from") << " " << source << " " << _("to") << " " << tgt << '\n'; + } + targetImage->setComment(sourceImage->comment()); + } + if (rc < 0) + try { + targetImage->writeMetadata(); + rc = 0; + } catch (const Exiv2::Error& e) { + std::cerr << tgt << ": " << _("Could not write metadata to file") << ": " << e << "\n"; + rc = 1; + } + + // if we used a temporary target, copy it to stdout + if (rc == 0 && bStdout) { + _setmode(_fileno(stdout), O_BINARY); + if (auto f = std::ifstream(target, std::ios::binary)) { + std::vector buffer(8 * 1024); + + while (f.read(buffer.data(), buffer.size()) || f.gcount() > 0) { + std::fwrite(buffer.data(), 1, static_cast(f.gcount()), stdout); + } + } + } + + // delete temporary target + if (bStdout) + fs::remove(target.c_str()); + + return rc; +} // metacopy + +void replace(std::string& text, const std::string& searchText, const std::string& replaceText) { + std::string::size_type index = 0; + while ((index = text.find(searchText, index)) != std::string::npos) { + text.replace(index, searchText.length(), replaceText.c_str(), replaceText.length()); + index++; + } +} + +int renameFile(std::string& newPath, const tm* tm, Exiv2::ExifData& exifData) { + auto p = fs::path(newPath); + std::string path = newPath; + auto oldFsPath = fs::path(path); + std::string format = Params::instance().format_; + std::string filename = p.stem().string(); + std::string basesuffix = ""; + const size_t pos = filename.find('.'); + if (pos != std::string::npos) + basesuffix = filename.substr(pos); + replace(format, ":basename:", p.stem().string()); + replace(format, ":basesuffix:", basesuffix); + replace(format, ":dirname:", p.parent_path().filename().string()); + replace(format, ":parentname:", p.parent_path().parent_path().filename().string()); + + const size_t max = 1024; + char basename[max] = {}; + if (strftime(basename, max, format.c_str(), tm) == 0) { + std::cerr << _("Filename format yields empty filename for the file") << " " << path << "\n"; + return 1; + } + + // get parent path with separator + // for concatenation of new file name, concatenation operator of std::filesystem::path is not used: + // On MSYS2 UCRT64 the path separator to be used in terminal is slash, but as concatenation operator + // a back slash will be added. Rename works but with verbose a path with different operators will be shown. + const size_t len = p.parent_path().string().length(); + std::string parent_path_sep = ""; + if (len > 0) + parent_path_sep = newPath.substr(0, len + 1); + + newPath = parent_path_sep + std::string(basename) + p.extension().string(); + + // rename using exiv2 tags + // is done after calling setting date/time: the value retrieved from tag might include something like %Y, which then + // should not be replaced by year + std::regex format_regex(":{1}?(Exif\\..*?):{1}?"); +#ifdef _WIN32 + std::string illegalChars = "\\/:*?\"<>|"; +#else + std::string illegalChars = "/:"; +#endif + std::regex_token_iterator rend; + std::regex_token_iterator token(format.begin(), format.end(), format_regex); + while (token != rend) { + Exiv2::Internal::enforce(token->str().length() >= 2, "token too short"); + std::string tag = token->str().substr(1, token->str().length() - 2); + const auto key = exifData.findKey(Exiv2::ExifKey(tag)); + std::string val = ""; + if (key != exifData.end()) { + val = key->print(&exifData); + if (val.length() == 0) { + std::cerr << path << ": " << _("Warning: ") << tag << _(" is empty.") << std::endl; + } else { + // replace characters invalid in file name + for (std::string::iterator it = val.begin(); it < val.end(); ++it) { + bool found = illegalChars.find(*it) != std::string::npos; + if (found) { + *it = '_'; + } + } + } + } else { + std::cerr << path << ": " << _("Warning: ") << tag << _(" is not included.") << std::endl; + } + replace(newPath, *token++, val); + } + + p = fs::path(newPath); + + if (p.parent_path() == oldFsPath.parent_path() && p.filename() == oldFsPath.filename()) { + if (Params::instance().verbose_) { + std::cout << _("This file already has the correct name") << '\n'; + } + return -1; + } + + bool go = true; + int seq = 1; + std::string s; + Params::FileExistsPolicy fileExistsPolicy = Params::instance().fileExistsPolicy_; + while (go) { + if (Exiv2::fileExists(newPath)) { + switch (fileExistsPolicy) { + case Params::overwritePolicy: + go = false; + break; + case Params::renamePolicy: + newPath = parent_path_sep + std::string(basename) + "_" + Exiv2::toString(seq++) + p.extension().string(); + break; + case Params::askPolicy: + std::cout << Params::instance().progname() << ": " << _("File") << " `" << newPath << "' " + << _("exists. [O]verwrite, [r]ename or [s]kip?") << " "; + std::cin >> s; + switch (s.front()) { + case 'o': + case 'O': + go = false; + break; + case 'r': + case 'R': + fileExistsPolicy = Params::renamePolicy; + newPath = parent_path_sep + std::string(basename) + "_" + Exiv2::toString(seq++) + p.extension().string(); + break; + default: // skip + return -1; + } + } + } else { + go = false; + } + } + + if (Params::instance().verbose_) { + std::cout << _("Renaming file to") << " " << newPath; + if (Params::instance().timestamp_) { + std::cout << ", " << _("updating timestamp"); + } + std::cout << '\n'; + } + + fs::rename(path, newPath); + return 0; +} + +std::string newFilePath(const std::string& path, const std::string& ext) { + auto p = fs::path(path); + auto directory = fs::path(Params::instance().directory_); + if (directory.empty()) + directory = p.parent_path(); + if (Exiv2::fileProtocol(path) != Exiv2::pFile) + directory.clear(); // use current directory for remote files + return (directory / (p.stem().string() + ext)).string(); +} + +int dontOverwrite(const std::string& path) { + if (path == "-") + return 0; + + if (!Params::instance().force_ && Exiv2::fileExists(path)) { + std::cout << Params::instance().progname() << ": " << _("Overwrite") << " `" << path << "'? "; + std::string s; + std::cin >> s; + if (s.front() != 'y' && s.front() != 'Y') + return 1; + } + return 0; +} + +std::ostream& operator<<(std::ostream& os, const std::pair& strAndWidth) { + const std::string& str(strAndWidth.first); + size_t minChCount(strAndWidth.second); + size_t count = mbstowcs(nullptr, str.c_str(), 0); // returns 0xFFFFFFFF on error + if (count < minChCount) { + minChCount += str.size() - count; + } + return os << std::setw(minChCount) << str; +} + +int printStructure(std::ostream& out, Exiv2::PrintStructureOption option, const std::string& path) { + if (!Exiv2::fileExists(path)) { + std::cerr << path << ": " << _("Failed to open the file") << "\n"; + return -1; + } + Exiv2::Image::UniquePtr image = Exiv2::ImageFactory::open(path); + image->printStructure(out, option); + return 0; +} +} // namespace diff --git a/app/actions.hpp b/app/actions.hpp new file mode 100644 index 0000000000..cf31139645 --- /dev/null +++ b/app/actions.hpp @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/*! + @file actions.hpp + @brief Implements base class Task, TaskFactory and the various supported + actions (derived from Task). + @author Andreas Huggel (ahu) + ahuggel@gmx.net + @date 11-Dec-03, ahu: created + */ +#ifndef ACTIONS_HPP_ +#define ACTIONS_HPP_ + +// ***************************************************************************** +#include "exiv2app.hpp" + +#include + +// ***************************************************************************** +// class declarations + +namespace Exiv2 { +class ExifData; +class Image; +class Metadatum; +class PreviewImage; +} // namespace Exiv2 + +// ***************************************************************************** +// namespace extensions +/// @brief Contains all action classes (task subclasses). +namespace Action { +//! Enumerates all tasks +enum TaskType { + none, + adjust, + print, + rename, + erase, + extract, + insert, + modify, + fixiso, + fixcom, +}; + +// ***************************************************************************** +// class definitions + +/*! + @brief Abstract base class for all concrete actions. + + Task provides a simple interface that actions must implement and a few + commonly used helpers. + */ +class Task { + public: + //! Shortcut for an auto pointer. + using UniquePtr = std::unique_ptr; + //! Virtual destructor. + virtual ~Task() = default; + + Task() = default; + Task(const Task&) = default; + Task& operator=(const Task&) = default; + + //! Virtual copy construction. + [[nodiscard]] virtual UniquePtr clone() const = 0; + + /// @brief Application interface to perform a task. + /// @param path Path of the file to process. + /// @return 0 if successful. + virtual int run(const std::string& path) = 0; + + bool setBinary(bool b) { + bool bResult = binary_; + binary_ = b; + return bResult; + } + + [[nodiscard]] bool binary() const { + return binary_; + } + + private: + //! copy binary_ from command-line params to task + bool binary_{false}; +}; // class Task + +/*! + @brief Task factory. + + Creates an instance of the task of the requested type. The factory is + implemented as a singleton, which can be accessed only through the static + member function instance(). +*/ +class TaskFactory { + public: + /*! + @brief Get access to the task factory. + Clients access the task factory exclusively through this method. (SINGLETON) + */ + static TaskFactory& instance(); + + ~TaskFactory() = default; + //! Prevent copy construction: not implemented. + TaskFactory(const TaskFactory&) = delete; + TaskFactory& operator=(const TaskFactory&) = delete; + + //! Destructor + void cleanup(); + + /*! + @brief Create a task. + + @param type Identifies the type of task to create. + @return An auto pointer that owns a task of the requested type. If + the task type is not supported, the pointer is 0. + @remark The caller of the function should check the content of the + returned auto pointer and take appropriate action (e.g., throw + an exception) if it is 0. + */ + Task::UniquePtr create(TaskType type); + + private: + //! Prevent construction other than through instance(). + TaskFactory(); + + //! List of task types and corresponding prototypes. + std::unordered_map registry_; +}; + +//! %Print the Exif (or other metadata) of a file to stdout +class Print : public Task { + public: + int run(const std::string& path) override; + [[nodiscard]] Task::UniquePtr clone() const override; + + //! Print the Jpeg comment + int printComment(); + //! Print list of available preview images + int printPreviewList(); + //! Print Exif summary information + int printSummary(); + //! Print Exif, IPTC and XMP metadata in user defined format + int printList(); + //! Return true if key should be printed, else false + static bool grepTag(const std::string& key); + //! Return true if key should be printed, else false + static bool keyTag(const std::string& key); + //! Print all metadata in a user defined format + int printMetadata(const Exiv2::Image* image); + //! Print a metadatum in a user defined format, return true if something was printed + bool printMetadatum(const Exiv2::Metadatum& md, const Exiv2::Image* image); + //! Print the label for a summary line + void printLabel(const std::string& label) const; + /*! + @brief Print one summary line with a label (if provided) and requested + data. A line break is printed only if a label is provided. + @return 1 if a line was written, 0 if the key was not found. + */ + int printTag(const Exiv2::ExifData& exifData, const std::string& key, const std::string& label = "") const; + //! Type for an Exiv2 Easy access function + using EasyAccessFct = Exiv2::ExifData::const_iterator (*)(const Exiv2::ExifData&); + /*! + @brief Print one summary line with a label (if provided) and requested + data. A line break is printed only if a label is provided. + @return 1 if a line was written, 0 if the information was not found. + */ + int printTag(const Exiv2::ExifData& exifData, EasyAccessFct easyAccessFct, const std::string& label = "", + EasyAccessFct easyAccessFctFallback = nullptr) const; + + private: + std::string path_; + int align_{0}; // for the alignment of the summary output +}; + +/// @brief %Rename a file to its metadata creation timestamp, in the specified format. +class Rename : public Task { + public: + int run(const std::string& path) override; + [[nodiscard]] Task::UniquePtr clone() const override; +}; // class Rename + +//! %Adjust the Exif (or other metadata) timestamps +class Adjust : public Task { + public: + int run(const std::string& path) override; + [[nodiscard]] Task::UniquePtr clone() const override; + + private: + int adjustDateTime(Exiv2::ExifData& exifData, const std::string& key, const std::string& path) const; + + int64_t adjustment_{0}; + int64_t yearAdjustment_{0}; + int64_t monthAdjustment_{0}; + int64_t dayAdjustment_{0}; + +}; // class Adjust + +/// @brief %Erase the entire exif data or only the thumbnail section. +class Erase : public Task { + public: + int run(const std::string& path) override; + [[nodiscard]] Task::UniquePtr clone() const override; + + /// @brief Delete the thumbnail image, incl IFD1 metadata from the file. + static int eraseThumbnail(Exiv2::Image* image); + + /// @brief Erase the complete Exif data block from the file. + static int eraseExifData(Exiv2::Image* image); + + /// @brief Erase all Iptc data from the file. + static int eraseIptcData(Exiv2::Image* image); + + /// @brief Erase Jpeg comment from the file. + static int eraseComment(Exiv2::Image* image); + + /// @brief Erase XMP packet from the file. + static int eraseXmpData(Exiv2::Image* image); + + /// @brief Erase ICCProfile from the file. + static int eraseIccProfile(Exiv2::Image* image); + + private: + std::string path_; +}; + +/// @brief %Extract the entire exif data or only the thumbnail section. +class Extract : public Task { + public: + int run(const std::string& path) override; + [[nodiscard]] Task::UniquePtr clone() const override; + + /*! + @brief Write the thumbnail image to a file. The filename is composed by + removing the suffix from the image filename and appending + "-thumb" and the appropriate suffix (".jpg" or ".tif"), depending + on the format of the Exif thumbnail image. + */ + [[nodiscard]] int writeThumbnail() const; + + /// @brief Write preview images to files. + [[nodiscard]] int writePreviews() const; + + /// @brief Write one preview image to a file. The filename is composed by removing the suffix from the image + /// filename and appending "-preview" and the appropriate suffix (".jpg" or ".tif"), depending on the + /// format of the Exif thumbnail image. + void writePreviewFile(const Exiv2::PreviewImage& pvImg, size_t num) const; + + /// @brief Write embedded iccProfile files. + [[nodiscard]] int writeIccProfile(const std::string& target) const; + + private: + std::string path_; +}; + +/// @brief %Insert the Exif data from corresponding *.exv files. +class Insert : public Task { + public: + int run(const std::string& path) override; + [[nodiscard]] Task::UniquePtr clone() const override; + + /*! + @brief Insert a Jpeg thumbnail image from a file into file \em path. + The filename of the thumbnail is expected to be the image + filename (\em path) minus its suffix plus "-thumb.jpg". + */ + static int insertThumbnail(const std::string& path); + + /// @brief Insert an XMP packet from a xmpPath into file \em path. + static int insertXmpPacket(const std::string& path, const std::string& xmpPath); + + /// @brief Insert xmp from a DataBuf into file \em path. + static int insertXmpPacket(const std::string& path, const Exiv2::DataBuf& xmpBlob, bool usePacket = false); + + /// @brief Insert an ICC profile from iccPath into file \em path. + static int insertIccProfile(const std::string& path, const std::string& iccPath); + + /// @brief Insert an ICC profile from binary DataBuf into file \em path. + static int insertIccProfile(const std::string& path, Exiv2::DataBuf&& iccProfileBlob); +}; + +/// @brief %Modify the Exif data according to the commands in the modification table. +class Modify : public Task { + public: + int run(const std::string& path) override; + [[nodiscard]] Task::UniquePtr clone() const override; + //! Apply modification commands to the \em pImage, return 0 if successful. + static int applyCommands(Exiv2::Image* pImage); + + private: + //! Add a metadatum to \em pImage according to \em modifyCmd + static int addMetadatum(Exiv2::Image* pImage, const ModifyCmd& modifyCmd); + //! Set a metadatum in \em pImage according to \em modifyCmd + static int setMetadatum(Exiv2::Image* pImage, const ModifyCmd& modifyCmd); + //! Delete a metadatum from \em pImage according to \em modifyCmd + static void delMetadatum(Exiv2::Image* pImage, const ModifyCmd& modifyCmd); + //! Register an XMP namespace according to \em modifyCmd + static void regNamespace(const ModifyCmd& modifyCmd); +}; + +/// @brief %Copy ISO settings from any of the Nikon makernotes to the regular Exif tag, Exif.Photo.ISOSpeedRatings. +class FixIso : public Task { + public: + int run(const std::string& path) override; + [[nodiscard]] Task::UniquePtr clone() const override; + + private: + std::string path_; +}; + +/// @brief Fix the character encoding of Exif UNICODE user comments. +/// +/// Decodes the comment using the auto-detected or specified character encoding and writes it back in UCS-2. +class FixCom : public Task { + public: + int run(const std::string& path) override; + [[nodiscard]] Task::UniquePtr clone() const override; + + private: + std::string path_; +}; + +} // namespace Action + +#endif // #ifndef ACTIONS_HPP_ diff --git a/app/app_utils.cpp b/app/app_utils.cpp new file mode 100644 index 0000000000..e2fedf9996 --- /dev/null +++ b/app/app_utils.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "app_utils.hpp" +#include +#include +#include +#include + +namespace Util { +bool strtol(const char* nptr, int64_t& n) { + if (!nptr || *nptr == '\0') + return false; + char* endptr = nullptr; + long long tmp = std::strtoll(nptr, &endptr, 10); + if (*endptr != '\0') + return false; + // strtoll returns LLONG_MAX or LLONG_MIN if an overflow occurs. + if (tmp == LLONG_MAX || tmp == LLONG_MIN) + return false; + if (tmp < std::numeric_limits::min()) + return false; + if (tmp > std::numeric_limits::max()) + return false; + n = static_cast(tmp); + return true; +} + +} // namespace Util diff --git a/app/app_utils.hpp b/app/app_utils.hpp new file mode 100644 index 0000000000..2731f52de9 --- /dev/null +++ b/app/app_utils.hpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef APP_UTILS_HPP_ +#define APP_UTILS_HPP_ + +#include + +namespace Util { +/*! + @brief Convert a C string to an int64_t value, which is returned in n. + Returns true if the conversion is successful, else false. + n is not modified if the conversion is unsuccessful. See strtol(2). + */ +bool strtol(const char* nptr, int64_t& n); +} // namespace Util + +#endif // APP_UTILS_HPP_ diff --git a/app/exiv2.cpp b/app/exiv2.cpp new file mode 100644 index 0000000000..b7aadc1770 --- /dev/null +++ b/app/exiv2.cpp @@ -0,0 +1,1502 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +// include local header files which are not part of libexiv2 +#include "actions.hpp" +#include "app_utils.hpp" +#include "exiv2app.hpp" + +#include "convert.hpp" +#include "getopt.hpp" +#include "i18n.h" // NLS support. +#include "utils.hpp" +#include "xmp_exiv2.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +namespace fs = std::filesystem; + +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#define _strdup strdup +#endif + +#ifdef EXV_ENABLE_NLS +#include +#endif + +// ***************************************************************************** +// local declarations +namespace { +constexpr auto emptyYodAdjust_ = std::array{ + Params::YodAdjust{false, "-Y", 0}, + Params::YodAdjust{false, "-O", 0}, + Params::YodAdjust{false, "-D", 0}, +}; + +//! List of all command identifiers and corresponding strings +constexpr struct CmdIdAndString { + CmdId cmdId_; + const char* string_; + //! Comparison operator for \em string + bool operator==(const std::string& string) const { + return string == string_; + } +} cmdIdAndString[] = { + {CmdId::add, "add"}, + {CmdId::set, "set"}, + {CmdId::del, "del"}, + {CmdId::reg, "reg"}, + {CmdId::invalid, "invalidCmd"}, // End of list marker +}; + +// Return a command Id for a command string +CmdId commandId(const std::string& cmdString); + +// Evaluate [-]HH[:MM[:SS]], returns true and sets time to the value +// in seconds if successful, else returns false. +bool parseTime(const std::string& ts, int64_t& time); + +/*! + @brief Parse the oparg string into a bitmap of common targets. + @param optArg Option arguments + @param action Action being processed + @return A bitmap of common targets or -1 in case of a parse error + */ +int64_t parseCommonTargets(const std::string& optArg, const std::string& action); + +/*! + @brief Parse numbers separated by commas into container + @param previewNumbers Container for the numbers + @param optArg Option arguments + @param j Starting index into optArg + @return Number of characters processed + */ +int parsePreviewNumbers(Params::PreviewNumbers& previewNumbers, const std::string& optArg, int j); + +/*! + @brief Parse metadata modification commands from multiple files + @param modifyCmds Reference to a structure to store the parsed commands + @param cmdFiles Container with the file names + */ +bool parseCmdFiles(ModifyCmds& modifyCmds, const Params::CmdFiles& cmdFiles); + +/*! + @brief Parse metadata modification commands from a container of commands + @param modifyCmds Reference to a structure to store the parsed commands + @param cmdLines Container with the commands + */ +bool parseCmdLines(ModifyCmds& modifyCmds, const Params::CmdLines& cmdLines); + +/*! + @brief Parse one line of the command file + @param modifyCmd Reference to a command structure to store the parsed + command + @param line Input line + @param num Line number (used for error output) + */ +bool parseLine(ModifyCmd& modifyCmd, const std::string& line, int num); + +/*! + @brief Parses a string containing backslash-escapes + @param input Input string, assumed to be UTF-8 + */ +std::string parseEscapes(const std::string& input); +} // namespace + +// ***************************************************************************** +// Main +int main(int argc, char* const argv[]) { +#ifdef EXV_ENABLE_NLS + setlocale(LC_ALL, ""); + auto localeDir = []() -> std::string { + fs::path ret = EXV_LOCALEDIR; + if constexpr (EXV_LOCALEDIR[0] != '/') + ret = fs::path(Exiv2::getProcessPath()) / EXV_LOCALEDIR; + return ret.string(); + }(); + bindtextdomain(EXV_PACKAGE_NAME, localeDir.c_str()); + textdomain(EXV_PACKAGE_NAME); +#endif + + // Handle command line arguments + Params& params = Params::instance(); + if (params.getopt(argc, argv)) { + params.usage(); + return 1; + } + if (params.help_) { + params.help(); + return 0; + } + if (params.version_) { + Params::version(params.verbose_); + return 0; + } + + int returnCode = EXIT_SUCCESS; + + try { + // Create the required action class + auto task = Action::TaskFactory::instance().create(static_cast(params.action_)); + + // Process all files + auto filesCount = params.files_.size(); + if (params.action_ & Action::extract && params.target_ & Params::ctStdInOut && filesCount > 1) { + std::cerr << params.progname() << ": " << _("Only one file is allowed when extracting to stdout") << '\n'; + returnCode = EXIT_FAILURE; + } else { + int w = [=]() { + if (filesCount > 9) { + if (filesCount > 99) + return 3; + return 2; + } + return 1; + }(); + int n = 1; + for (const auto& file : params.files_) { + // If extracting to stdout then ignore verbose + if (params.verbose_ && !(params.action_ & Action::extract && params.target_ & Params::ctStdInOut)) { + std::cout << _("File") << " " << std::setw(w) << std::right << n++ << "/" << filesCount << ": " << file + << '\n'; + } + task->setBinary(params.binary_); + int ret = task->run(file); + if (returnCode == EXIT_SUCCESS) + returnCode = ret; + } + + Action::TaskFactory::instance().cleanup(); + } + } catch (const std::exception& exc) { + std::cerr << "Uncaught exception: " << exc.what() << '\n'; + returnCode = EXIT_FAILURE; + } + + // Return a positive one byte code for better consistency across platforms + return static_cast(returnCode) % 256; +} // main + +// ***************************************************************************** +// class Params + +Params::Params() : + optstring_(":hVvqfbuktTFa:Y:O:D:r:p:P:d:e:i:c:m:M:l:S:g:K:n:Q:"), + target_(ctExif | ctIptc | ctComment | ctXmp), + yodAdjust_(emptyYodAdjust_), + format_("%Y%m%d_%H%M%S") { +} + +Params& Params::instance() { + static Params instance_; + return instance_; +} + +void Params::version(bool verbose, std::ostream& os) { + os << EXV_PACKAGE_STRING << '\n'; + if (Params::instance().greps_.empty() && !verbose) { + os << "\n" + << _("This program is free software; you can redistribute it and/or\n" + "modify it under the terms of the GNU General Public License\n" + "as published by the Free Software Foundation; either version 2\n" + "of the License, or (at your option) any later version.\n") + << "\n" + << _("This program is distributed in the hope that it will be useful,\n" + "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + "GNU General Public License for more details.\n") + << "\n" + << _("You should have received a copy of the GNU General Public\n" + "License along with this program; if not, write to the Free\n" + "Software Foundation, Inc., 51 Franklin Street, Fifth Floor,\n" + "Boston, MA 02110-1301 USA\n"); + } + + if (verbose) + Exiv2::dumpLibraryInfo(os, Params::instance().greps_); +} + +void Params::usage(std::ostream& os) const { + os << _("Usage:") << " " << progname() << " " << _("[ option [ arg ] ]+ [ action ] file ...\n\n") + << _("Image metadata manipulation tool.\n"); +} + +void Params::help(std::ostream& os) const { + usage(os); + os << _("\nWhere file is one or more files, optionally containing a URL\n" + "(http, https, ftp, sftp, data or file) or wildcard\n") + << _("\nActions:\n") + << _(" pr | print Print image metadata (default is a summary). This is the default\n" + " action\n") + << _(" ad | adjust Adjust Exif timestamps by the given time. Requires\n" + " at least one of -a, -Y, -O or -D\n") + << _(" rm | delete Deletes image metadata, use -d to choose type to delete\n" + " (default is all)\n") + << _(" in | insert Insert metadata from .exv, .xmp, thumbnail or .icc file.\n" + " Use option -S to change the suffix of the input files and\n" + " -l to change the location\n") + << _(" ex | extract Extract metadata to .exv, .xmp, preview image, thumbnail,\n" + " or ICC profile. Use option -S to change the suffix of the input\n" + " files and -l to change the location\n") + << _(" mv | rename Rename files and/or set file timestamps according to the\n" + " Exif timestamps. The filename format can be set with\n" + " -r format, timestamp options are controlled with -t and -T\n") + << _(" mo | modify Apply commands to modify the Exif, IPTC and XMP metadata.\n" + " Requires option -m or -M\n") + << _(" fi | fixiso Copy ISO setting from Canon and Nikon makernotes, to the\n" + " standard Exif tag\n") + << _(" fc | fixcom Convert the Unicode Exif user comment to UCS-2. The current\n" + " character encoding can be specified with the -n option\n") + << _("\nOptions:\n") << _(" -h Display this help and exit\n") + << _(" -V Show the program version and exit\n") << _(" -v Be verbose during the program run\n") + << _(" -q Silence warnings and error messages (quiet)\n") + << _(" -Q lvl Set log-level to d(ebug), i(nfo), w(arning), e(rror) or m(ute)\n") + << _(" -b Obsolete, reserved for use with the test suit\n") + << _(" -u Show unknown tags (e.g., Exif.SonyMisc3c.0x022b)\n") + << _(" -g str Only output where 'str' matches in output text (grep)\n" + " Append /i to 'str' for case insensitive\n") + << _(" -K key Only output where 'key' exactly matches tag's key\n") + << _(" -n enc Character set to decode Exif Unicode user comments\n") + << _(" -k Preserve file timestamps when updating files (keep)\n") + << _(" -t Set the file timestamp from Exif metadata when renaming (overrides -k)\n") + << _(" -T Only set the file timestamp from Exif metadata ('rename' action)\n") + << _(" -f Do not prompt before overwriting existing files (force)\n") + << _(" -F Do not prompt before renaming files (Force)\n") + << _(" -a time Time adjustment in the format [+|-]HH[:MM[:SS]]. For 'adjust' action\n") + << _(" -Y yrs Year adjustment with the 'adjust' action\n") + << _(" -O mon Month adjustment with the 'adjust' action\n") + << _(" -D day Day adjustment with the 'adjust' action\n") + << _(" -p mode Print mode for the 'print' action. Possible modes are:\n") + << _(" s : A summary of the Exif metadata (the default)\n") + << _(" a : Exif, IPTC and XMP tags (shortcut for -Pkyct)\n") + << _(" e : Exif tags (shortcut for -PEkycv)\n") + << _(" t : Interpreted (translated) Exif tags (-PEkyct)\n") + << _(" v : Plain (untranslated) Exif tags values (-PExgnycv)\n") + << _(" h : Hex dump of the Exif tags (-PExgnycsh)\n") << _(" i : IPTC tags (-PIkyct)\n") + << _(" x : XMP tags (-PXkyct)\n") << _(" c : JPEG comment\n") + << _(" p : List available image preview, sorted by size\n") + << _(" C : Print ICC profile\n") + << _(" R : Recursive print structure of image (debug build only)\n") + << _(" S : Print structure of image (limited file types)\n") + << _(" X : Extract \"raw\" XMP\n") + << _(" -P flgs Print flags for fine control of tag lists ('print' action):\n") + << _(" E : Exif tags\n") << _(" I : IPTC tags\n") << _(" X : XMP tags\n") + << _(" x : Tag number for Exif or IPTC tags (in hexadecimal)\n") + << _(" g : Group name (e.g. Exif.Photo.UserComment, Photo)\n") + << _(" k : Key (e.g. Exif.Photo.UserComment)\n") + << _(" l : Tag label (e.g. Exif.Photo.UserComment, 'User comment')\n") + << _(" d : Tag description\n") + << _(" n : Tag name (e.g. Exif.Photo.UserComment, UserComment)\n") << _(" y : Type\n") + << _(" y : Type\n") << _(" c : Number of components (count)\n") + << _(" s : Size in bytes of vanilla value (may include NULL)\n") + << _(" v : Plain data value of untranslated (vanilla)\n") + << _(" V : Plain data value, data type and the word 'set'\n") + << _(" t : Interpreted (translated) human readable values\n") + << _(" h : Hex dump of the data\n") + << _(" -d tgt1 Delete target(s) for the 'delete' action. Possible targets are:\n") + << _(" a : All supported metadata (the default)\n") << _(" e : Exif tags\n") + << _(" t : Exif thumbnail only\n") << _(" i : IPTC tags\n") + << _(" x : XMP tags\n") << _(" c : JPEG comment\n") << _(" C : ICC Profile\n") + << _(" c : All IPTC data (any broken multiple IPTC blocks)\n") + << _(" - : Input from stdin\n") + << _(" -i tgt2 Insert target(s) for the 'insert' action. Possible targets are\n") + << _(" a : All supported metadata (the default)\n") << _(" e : Exif tags\n") + << _(" t : Exif thumbnail only (JPEGs only from -thumb.jpg)\n") + << _(" i : IPTC tags\n") << _(" x : XMP tags\n") << _(" c : JPEG comment\n") + << _(" C : ICC Profile, from .icc\n") << _(" X : XMP sidecar from file .xmp\n") + << _(" XX: \"raw\" metadata from .exv. XMP default, optional Exif and IPTC\n") + << _(" - : Input from stdin\n") + << _(" -e tgt3 Extract target(s) for the 'extract' action. Possible targets\n") + << _(" a : All supported metadata (the default)\n") << _(" e : Exif tags\n") + << _(" t : Exif thumbnail only (to -thumb.jpg)\n") << _(" i : IPTC tags\n") + << _(" x : XMP tags\n") << _(" c : JPEG comment\n") + << _(" pN: Extract N'th preview image to -preview.\n") + << _(" C : ICC Profile, to .icc\n") << _(" X : XMP sidecar to .xmp\n") + << _(" XX: \"raw\" metadata to .exv. XMP default, optional Exif and IPTC\n") + << _(" - : Output to stdin\n") + << _(" -r fmt Filename format for the 'rename' action. The format string\n") + << _(" follows strftime(3). The following keywords are also supported:\n") + << _(" :basename: - original filename without extension\n") + << _(" :basesuffix: - suffix in original filename, starts with first dot and ends before extension\n") + << _(" :dirname: - name of the directory holding the original file\n") + << _(" :parentname: - name of parent directory\n") << _(" Default 'fmt' is %Y%m%d_%H%M%S\n") + << _(" -c txt JPEG comment string to set in the image.\n") + << _(" -m cmdf Applies commands in 'cmdf' file, for the modify action (see -M for format).\n") + << _(" -M cmd Command line for the modify action. The format is:\n") + << _(" ( (set | add) [[] ] |\n") << _(" del [] |\n") + << _(" reg prefix namespace )\n") + << _(" -l dir Location (directory) for files to be inserted from or extracted to.\n") + << _(" -S suf Use suffix 'suf' for source files for insert action.\n") << _("\nExamples:\n") + << _(" exiv2 -pe image.dng *.jp2\n" + " Print all Exif tags in image.dng and all .jp2 files\n") + << _(" exiv2 -g date/i https://clanmills.com/Stonehenge.jpg\n" + " Print all tags in file, where key contains 'date' (case insensitive)\n") + << _(" exiv2 -M\"set Xmp.dc.subject XmpBag Sky\" image.tiff\n" + " Set (or add if missing) value to tag in file\n\n"); +} // Params::help + +int Params::option(int opt, const std::string& optArg, int optOpt) { + int rc = 0; + switch (opt) { + case 'h': + help_ = true; + break; + case 'V': + version_ = true; + break; + case 'v': + verbose_ = true; + break; + case 'q': + Exiv2::LogMsg::setLevel(Exiv2::LogMsg::mute); + break; + case 'Q': + rc = setLogLevel(optArg); + break; + case 'k': + preserve_ = true; + break; + case 'b': + binary_ = true; + break; + case 'u': + unknown_ = false; + break; + case 'f': + force_ = true; + fileExistsPolicy_ = overwritePolicy; + break; + case 'F': + force_ = true; + fileExistsPolicy_ = renamePolicy; + break; + case 'g': + rc = evalGrep(optArg); + break; + case 'K': + rc = evalKey(optArg); + printMode_ = pmList; + break; + case 'n': + charset_ = optArg; + break; + case 'r': + rc = evalRename(opt, optArg); + break; + case 't': + rc = evalRename(opt, optArg); + break; + case 'T': + rc = evalRename(opt, optArg); + break; + case 'a': + rc = evalAdjust(optArg); + break; + case 'Y': + rc = evalYodAdjust(yodYear, optArg); + break; + case 'O': + rc = evalYodAdjust(yodMonth, optArg); + break; + case 'D': + rc = evalYodAdjust(yodDay, optArg); + break; + case 'p': + rc = evalPrint(optArg); + break; + case 'P': + rc = evalPrintFlags(optArg); + break; + case 'd': + rc = evalDelete(optArg); + break; + case 'e': + rc = evalExtract(optArg); + break; + case 'C': + rc = evalExtract(optArg); + break; + case 'i': + rc = evalInsert(optArg); + break; + case 'c': + rc = evalModify(opt, optArg); + break; + case 'm': + rc = evalModify(opt, optArg); + break; + case 'M': + rc = evalModify(opt, optArg); + break; + case 'l': + directory_ = optArg; + break; + case 'S': + suffix_ = optArg; + break; + case ':': + std::cerr << progname() << ": " << _("Option") << " -" << static_cast(optOpt) << " " + << _("requires an argument\n"); + rc = 1; + break; + case '?': + std::cerr << progname() << ": " << _("Unrecognized option") << " -" << static_cast(optOpt) << "\n"; + rc = 1; + break; + default: + std::cerr << progname() << ": " << _("getopt returned unexpected character code") << " " << std::hex << opt + << "\n"; + rc = 1; + break; + } + return rc; +} // Params::option + +int Params::setLogLevel(const std::string& optArg) { + int rc = 0; + const auto logLevel = static_cast(tolower(optArg[0])); + switch (logLevel) { + case 'd': + Exiv2::LogMsg::setLevel(Exiv2::LogMsg::debug); + break; + case 'i': + Exiv2::LogMsg::setLevel(Exiv2::LogMsg::info); + break; + case 'w': + Exiv2::LogMsg::setLevel(Exiv2::LogMsg::warn); + break; + case 'e': + Exiv2::LogMsg::setLevel(Exiv2::LogMsg::error); + break; + case 'm': + Exiv2::LogMsg::setLevel(Exiv2::LogMsg::mute); + break; + default: + std::cerr << progname() << ": " << _("Option") << " -Q: " << _("Invalid argument") << " \"" << optArg << "\"\n"; + rc = 1; + break; + } + return rc; +} // Params::setLogLevel + +int Params::evalGrep(const std::string& optArg) { + // check that string ends in "/i" + bool bIgnoreCase = optArg.size() > 2 && optArg.back() == 'i' && optArg[optArg.size() - 2] == '/'; + auto pattern = bIgnoreCase ? optArg.substr(0, optArg.size() - 2) : optArg; + + try { + // use POSIX syntax, optimize for faster matching, treat all sub expressions as unnamed + auto flags = std::regex::basic | std::regex::optimize | std::regex::nosubs; + flags = bIgnoreCase ? flags | std::regex::icase : flags; + // try and emplace regex into vector + // might throw if invalid pattern + greps_.emplace_back(pattern, flags); + } catch (std::regex_error const&) { + // there was an error compiling the regexp + std::cerr << progname() << ": " << _("Option") << " -g: " << _("Invalid regexp") << " \"" << optArg << "\n"; + return 1; + } + + return 0; +} // Params::evalGrep + +int Params::evalKey(const std::string& optArg) { + int result = 0; + keys_.push_back(optArg); + return result; +} // Params::evalKey + +int Params::evalRename(int opt, const std::string& optArg) { + int rc = 0; + switch (action_) { + case Action::none: + action_ = Action::rename; + switch (opt) { + case 'r': + format_ = optArg; + formatSet_ = true; + break; + case 't': + timestamp_ = true; + break; + case 'T': + timestampOnly_ = true; + break; + } + break; + case Action::rename: + if (opt == 'r' && (formatSet_ || timestampOnly_)) { + std::cerr << progname() << ": " << _("Ignoring surplus option") << " -r \"" << optArg << "\"\n"; + } else { + format_ = optArg; + formatSet_ = true; + } + break; + default: + std::cerr << progname() << ": " << _("Option") << " -" << static_cast(opt) << " " + << _("is not compatible with a previous option\n"); + rc = 1; + break; + } + return rc; +} // Params::evalRename + +int Params::evalAdjust(const std::string& optArg) { + int rc = 0; + switch (action_) { + case Action::none: + case Action::adjust: + if (adjust_) { + std::cerr << progname() << ": " << _("Ignoring surplus option -a") << " " << optArg << "\n"; + break; + } + action_ = Action::adjust; + adjust_ = parseTime(optArg, adjustment_); + if (!adjust_) { + std::cerr << progname() << ": " << _("Error parsing -a option argument") << " `" << optArg << "'\n"; + rc = 1; + } + break; + default: + std::cerr << progname() << ": " << _("Option -a is not compatible with a previous option\n"); + rc = 1; + break; + } + return rc; +} // Params::evalAdjust + +int Params::evalYodAdjust(const Yod& yod, const std::string& optArg) { + int rc = 0; + switch (action_) { + case Action::none: + case Action::adjust: + if (yodAdjust_[yod].flag_) { + std::cerr << progname() << ": " << _("Ignoring surplus option") << " " << yodAdjust_[yod].option_ << " " + << optArg << "\n"; + break; + } + action_ = Action::adjust; + yodAdjust_[yod].flag_ = true; + if (!Util::strtol(optArg.c_str(), yodAdjust_[yod].adjustment_)) { + std::cerr << progname() << ": " << _("Error parsing") << " " << yodAdjust_[yod].option_ << " " + << _("option argument") << " `" << optArg << "'\n"; + rc = 1; + } + break; + default: + std::cerr << progname() << ": " << _("Option") << " " << yodAdjust_[yod].option_ << " " + << _("is not compatible with a previous option\n"); + rc = 1; + break; + } + return rc; +} // Params::evalYodAdjust + +int Params::evalPrint(const std::string& optArg) { + int rc = 0; + switch (action_) { + case Action::none: + switch (optArg[0]) { + case 's': + action_ = Action::print; + printMode_ = pmSummary; + break; + case 'a': + rc = evalPrintFlags("kyct"); + break; + case 'e': + rc = evalPrintFlags("Ekycv"); + break; + case 't': + rc = evalPrintFlags("Ekyct"); + break; + case 'v': + rc = evalPrintFlags("Exgnycv"); + break; + case 'h': + rc = evalPrintFlags("Exgnycsh"); + break; + case 'i': + rc = evalPrintFlags("Ikyct"); + break; + case 'x': + rc = evalPrintFlags("Xkyct"); + break; + case 'c': + action_ = Action::print; + printMode_ = pmComment; + break; + case 'p': + action_ = Action::print; + printMode_ = pmPreview; + break; + case 'C': + action_ = Action::print; + printMode_ = pmIccProfile; + break; + case 'R': +#ifdef NDEBUG + std::cerr << progname() << ": " << _("Action not available in Release mode") << ": '" << optArg << "'\n"; + rc = 1; +#else + action_ = Action::print; + printMode_ = pmRecursive; +#endif + break; + case 'S': + action_ = Action::print; + printMode_ = pmStructure; + break; + case 'X': + action_ = Action::print; + printMode_ = pmXMP; + break; + default: + std::cerr << progname() << ": " << _("Unrecognized print mode") << " `" << optArg << "'\n"; + rc = 1; + break; + } + break; + case Action::print: + std::cerr << progname() << ": " << _("Ignoring surplus option -p") << optArg << "\n"; + break; + default: + std::cerr << progname() << ": " << _("Option -p is not compatible with a previous option\n"); + rc = 1; + break; + } + return rc; +} // Params::evalPrint + +int Params::evalPrintFlags(const std::string& optArg) { + int rc = 0; + switch (action_) { + case Action::none: + action_ = Action::print; + printMode_ = pmList; + for (auto&& i : optArg) { + switch (i) { + case 'E': + printTags_ |= MetadataId::exif; + break; + case 'I': + printTags_ |= MetadataId::iptc; + break; + case 'X': + printTags_ |= MetadataId::xmp; + break; + case 'x': + printItems_ |= prTag; + break; + case 'g': + printItems_ |= prGroup; + break; + case 'k': + printItems_ |= prKey; + break; + case 'l': + printItems_ |= prLabel; + break; + case 'n': + printItems_ |= prName; + break; + case 'y': + printItems_ |= prType; + break; + case 'c': + printItems_ |= prCount; + break; + case 's': + printItems_ |= prSize; + break; + case 'v': + printItems_ |= prValue; + break; + case 't': + printItems_ |= prTrans; + break; + case 'h': + printItems_ |= prHex; + break; + case 'V': + printItems_ |= prSet | prKey | prType | prValue; + break; + case 'd': + printItems_ |= prDesc; + break; + default: + std::cerr << progname() << ": " << _("Unrecognized print item") << " `" << i << "'\n"; + rc = 1; + break; + } + } + break; + case Action::print: + std::cerr << progname() << ": " << _("Ignoring surplus option -P") << optArg << "\n"; + break; + default: + std::cerr << progname() << ": " << _("Option -P is not compatible with a previous option\n"); + rc = 1; + break; + } + return rc; +} // Params::evalPrintFlags + +int Params::evalDelete(const std::string& optArg) { + switch (action_) { + case Action::none: + action_ = Action::erase; + target_ = CommonTarget{0}; + [[fallthrough]]; + case Action::erase: { + const auto rc = parseCommonTargets(optArg, "erase"); + if (rc > 0) { + target_ |= static_cast(rc); + return 0; + } + return 1; + } + default: + std::cerr << progname() << ": " << _("Option -d is not compatible with a previous option\n"); + return 1; + } +} // Params::evalDelete + +int Params::evalExtract(const std::string& optArg) { + switch (action_) { + case Action::none: + case Action::modify: + action_ = Action::extract; + target_ = CommonTarget{0}; + [[fallthrough]]; + case Action::extract: { + const auto rc = parseCommonTargets(optArg, "extract"); + if (rc > 0) { + target_ |= static_cast(rc); + return 0; + } + return 1; + } + default: + std::cerr << progname() << ": " << _("Option -e is not compatible with a previous option\n"); + return 1; + } +} // Params::evalExtract + +int Params::evalInsert(const std::string& optArg) { + switch (action_) { + case Action::none: + case Action::modify: + action_ = Action::insert; + target_ = CommonTarget{0}; + [[fallthrough]]; + case Action::insert: { + const auto rc = parseCommonTargets(optArg, "insert"); + if (rc > 0) { + target_ |= static_cast(rc); + return 0; + } + return 1; + } + default: + std::cerr << progname() << ": " << _("Option -i is not compatible with a previous option\n"); + return 1; + } +} // Params::evalInsert + +int Params::evalModify(int opt, const std::string& optArg) { + switch (action_) { + case Action::none: + action_ = Action::modify; + [[fallthrough]]; + case Action::modify: + case Action::extract: + case Action::insert: + if (opt == 'c') + jpegComment_ = parseEscapes(optArg); + if (opt == 'm') + cmdFiles_.push_back(optArg); // parse the files later + if (opt == 'M') + cmdLines_.push_back(optArg); // parse the commands later + return 0; + default: + std::cerr << progname() << ": " << _("Option") << " -" << static_cast(opt) << " " + << _("is not compatible with a previous option\n"); + return 1; + } +} // Params::evalModify + +int Params::nonoption(const std::string& argv) { + int rc = 0; + bool action = false; + if (first_) { + // The first non-option argument must be the action + first_ = false; + if (argv == "ad" || argv == "adjust") { + if (action_ != Action::none && action_ != Action::adjust) { + std::cerr << progname() << ": " << _("Action adjust is not compatible with the given options\n"); + rc = 1; + } + action = true; + action_ = Action::adjust; + } + if (argv == "pr" || argv == "print") { + if (action_ != Action::none && action_ != Action::print) { + std::cerr << progname() << ": " << _("Action print is not compatible with the given options\n"); + rc = 1; + } + action = true; + action_ = Action::print; + } + if (argv == "rm" || argv == "delete") { + if (action_ != Action::none && action_ != Action::erase) { + std::cerr << progname() << ": " << _("Action delete is not compatible with the given options\n"); + rc = 1; + } + action = true; + action_ = Action::erase; + } + if (argv == "ex" || argv == "extract") { + if (action_ != Action::none && action_ != Action::extract && action_ != Action::modify) { + std::cerr << progname() << ": " << _("Action extract is not compatible with the given options\n"); + rc = 1; + } + action = true; + action_ = Action::extract; + } + if (argv == "in" || argv == "insert") { + if (action_ != Action::none && action_ != Action::insert && action_ != Action::modify) { + std::cerr << progname() << ": " << _("Action insert is not compatible with the given options\n"); + rc = 1; + } + action = true; + action_ = Action::insert; + } + if (argv == "mv" || argv == "rename") { + if (action_ != Action::none && action_ != Action::rename) { + std::cerr << progname() << ": " << _("Action rename is not compatible with the given options\n"); + rc = 1; + } + action = true; + action_ = Action::rename; + } + if (argv == "mo" || argv == "modify") { + if (action_ != Action::none && action_ != Action::modify) { + std::cerr << progname() << ": " << _("Action modify is not compatible with the given options\n"); + rc = 1; + } + action = true; + action_ = Action::modify; + } + if (argv == "fi" || argv == "fixiso") { + if (action_ != Action::none && action_ != Action::fixiso) { + std::cerr << progname() << ": " << _("Action fixiso is not compatible with the given options\n"); + rc = 1; + } + action = true; + action_ = Action::fixiso; + } + if (argv == "fc" || argv == "fixcom" || argv == "fixcomment") { + if (action_ != Action::none && action_ != Action::fixcom) { + std::cerr << progname() << ": " << _("Action fixcom is not compatible with the given options\n"); + rc = 1; + } + action = true; + action_ = Action::fixcom; + } + if (action_ == Action::none) { + // if everything else fails, assume print as the default action + action_ = Action::print; + } + } + if (!action) { + files_.push_back(argv); + } + return rc; +} // Params::nonoption + +static size_t readFileToBuf(FILE* f, Exiv2::DataBuf& buf) { + const int buff_size = 4 * 1028; + std::vector bytes(buff_size); + size_t nBytes = 0; + bool more{true}; + std::array buff; + while (more) { + auto n = fread(buff.data(), 1, buff_size, f); + more = n > 0; + if (more) { + bytes.resize(nBytes + n); + std::copy_n(buff.begin(), n, bytes.begin() + nBytes); + nBytes += n; + } + } + + if (nBytes) { + buf.alloc(nBytes); + std::copy(bytes.begin(), bytes.end(), buf.begin()); + } + return nBytes; +} + +void Params::getStdin(Exiv2::DataBuf& buf) { + // copy stdin to stdinBuf + if (stdinBuf.empty()) { +#ifdef _WIN32 + DWORD fdwMode; + _setmode(_fileno(stdin), O_BINARY); + Sleep(300); + if (!GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &fdwMode)) { // failed: stdin has bytes! +#else + // http://stackoverflow.com/questions/34479795/make-c-not-wait-for-user-input/34479916#34479916 + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(STDIN_FILENO, &readfds); + timeval timeout = {1, 0}; // yes: set timeout seconds,microseconds + + // if we have something in the pipe, read it + if (select(1, &readfds, nullptr, nullptr, &timeout)) { +#endif +#ifdef DEBUG + std::cerr << "stdin has data" << '\n'; +#endif + readFileToBuf(stdin, stdinBuf); + } +#ifdef DEBUG + // this is only used to simulate reading from stdin when debugging + // to simulate exiv2 -pX foo.jpg | exiv2 -iXX- bar.jpg + // exiv2 -pX foo.jpg > ~/temp/stdin ; exiv2 -iXX- bar.jpg + if (stdinBuf.empty()) { + const char* path = "/Users/rmills/temp/stdin"; + FILE* f = fopen(path, "rb"); + if (f) { + readFileToBuf(f, stdinBuf); + fclose(f); + std::cerr << "read stdin from " << path << '\n'; + } + } +#endif +#ifdef DEBUG + std::cerr << "getStdin stdinBuf.size_ = " << stdinBuf.size() << '\n'; +#endif + } + + // copy stdinBuf to buf + if (!stdinBuf.empty()) { + buf.alloc(stdinBuf.size()); + std::copy(stdinBuf.begin(), stdinBuf.end(), buf.begin()); + } +#ifdef DEBUG + std::cerr << "getStdin stdinBuf.size_ = " << stdinBuf.size() << '\n'; +#endif + +} // Params::getStdin() + +int Params::getopt(int argc, char* const Argv[]) { + std::vector argv(argc + 1); + argv.back() = nullptr; + + const std::unordered_map longs{ + {"--adjust", "-a"}, {"--binary", "-b"}, {"--comment", "-c"}, {"--delete", "-d"}, {"--days", "-D"}, + {"--extract", "-e"}, {"--force", "-f"}, {"--Force", "-F"}, {"--grep", "-g"}, {"--help", "-h"}, + {"--insert", "-i"}, {"--keep", "-k"}, {"--key", "-K"}, {"--location", "-l"}, {"--modify", "-m"}, + {"--Modify", "-M"}, {"--encode", "-n"}, {"--months", "-O"}, {"--print", "-p"}, {"--Print", "-P"}, + {"--quiet", "-q"}, {"--log", "-Q"}, {"--rename", "-r"}, {"--suffix", "-S"}, {"--timestamp", "-t"}, + {"--Timestamp", "-T"}, {"--unknown", "-u"}, {"--verbose", "-v"}, {"--Version", "-V"}, {"--version", "-V"}, + {"--years", "-Y"}, + }; + + for (int i = 0; i < argc; i++) { + std::string arg(Argv[i]); + if (longs.contains(arg)) { + argv[i] = _strdup(longs.at(arg).c_str()); + } else { + argv[i] = _strdup(Argv[i]); + } + } + + int rc = Util::Getopt::getopt(argc, argv.data(), optstring_); + // Further consistency checks + if (help_ || version_) { + goto cleanup; + } + if (action_ == Action::none) { + // This shouldn't happen since print is taken as default action + std::cerr << progname() << ": " << _("An action must be specified\n"); + rc = 1; + } + if (action_ == Action::adjust && !adjust_ && !yodAdjust_[yodYear].flag_ && !yodAdjust_[yodMonth].flag_ && + !yodAdjust_[yodDay].flag_) { + std::cerr << progname() << ": " << _("Adjust action requires at least one -a, -Y, -O or -D option\n"); + rc = 1; + } + if (action_ == Action::modify && cmdFiles_.empty() && cmdLines_.empty() && jpegComment_.empty()) { + std::cerr << progname() << ": " << _("Modify action requires at least one -c, -m or -M option\n"); + rc = 1; + } + if (files_.empty()) { + std::cerr << progname() << ": " << _("At least one file is required\n"); + rc = 1; + } + // Parse command files + if (rc == 0 && !cmdFiles_.empty() && !parseCmdFiles(modifyCmds_, cmdFiles_)) { + std::cerr << progname() << ": " << _("Error parsing -m option arguments\n"); + rc = 1; + } + // Parse command lines + if (rc == 0 && !cmdLines_.empty() && !parseCmdLines(modifyCmds_, cmdLines_)) { + std::cerr << progname() << ": " << _("Error parsing -M option arguments\n"); + rc = 1; + } + if (rc == 0 && (!cmdFiles_.empty() || !cmdLines_.empty())) { + // We'll set them again, after reading the file + Exiv2::XmpProperties::unregisterNs(); + } + if (!directory_.empty() && action_ != Action::insert && action_ != Action::extract) { + std::cerr << progname() << ": " << _("-l option can only be used with extract or insert actions\n"); + rc = 1; + } + if (!suffix_.empty() && action_ != Action::insert) { + std::cerr << progname() << ": " << _("-S option can only be used with insert action\n"); + rc = 1; + } + if (timestamp_ && action_ != Action::rename) { + std::cerr << progname() << ": " << _("-t option can only be used with rename action\n"); + rc = 1; + } + if (timestampOnly_ && action_ != Action::rename) { + std::cerr << progname() << ": " << _("-T option can only be used with rename action\n"); + rc = 1; + } + +cleanup: + // cleanup the argument vector + for (int i = 0; i < argc; i++) + ::free(argv[i]); + + return rc; +} + +// ***************************************************************************** +// local implementations +namespace { +bool parseTime(const std::string& ts, int64_t& time) { + std::istringstream sts(ts); + int sign = 1; + int64_t hh = 0; + int64_t mm = 0; + int64_t ss = 0; + + // [-]HH part + if (!(sts >> hh)) + return false; + if (hh < 0) { + sign = -1; + hh *= -1; + } + // check for the -0 special case + if (hh == 0 && Exiv2::Internal::contains(ts, '-')) + sign = -1; + // MM part, if there is one + if (sts.peek() == ':') { + sts.ignore(); + if (!(sts >> mm) || mm > 59 || mm < 0) + return false; + } + // SS part, if there is one + if (sts.peek() == ':') { + sts.ignore(); + if (!(sts >> ss) || ss > 59 || ss < 0) + return false; + } + + time = sign * (hh * 3600 + mm * 60 + ss); + return true; +} // parseTime + +void printUnrecognizedArgument(const char argc, const std::string& action) { + std::cerr << Params::instance().progname() << ": " << _("Unrecognized ") << action << " " << _("target") << " `" + << argc << "'\n"; +} + +int64_t parseCommonTargets(const std::string& optArg, const std::string& action) { + int64_t rc = 0; + auto target = Params::CommonTarget{0}; + Params::CommonTarget all = Params::ctExif | Params::ctIptc | Params::ctComment | Params::ctXmp; + Params::CommonTarget extra = Params::ctXmpSidecar | Params::ctExif | Params::ctIptc | Params::ctXmp; + for (size_t i = 0; rc == 0 && i < optArg.size(); ++i) { + switch (optArg[i]) { + case 'e': + target |= Params::ctExif; + break; + case 'i': + target |= Params::ctIptc; + break; + case 'x': + target |= Params::ctXmp; + break; + case 'c': + target |= Params::ctComment; + break; + case 't': + target |= Params::ctThumb; + break; + case 'C': + target |= Params::ctIccProfile; + break; + case 'I': + target |= Params::ctIptcRaw; + break; + case '-': + target |= Params::ctStdInOut; + break; + case 'a': + target |= all; + break; + case 'X': + target |= extra; // -eX + if (i > 0) { // -eXX or -iXX + target |= Params::ctXmpRaw; + target = static_cast(target & ~extra); // turn off those bits + } + break; + + case 'p': { + if (action == "extract") { + i += static_cast( + parsePreviewNumbers(Params::instance().previewNumbers_, optArg, static_cast(i) + 1)); + target |= Params::ctPreview; + break; + } + printUnrecognizedArgument(optArg[i], action); + rc = -1; + break; + } + default: + printUnrecognizedArgument(optArg[i], action); + rc = -1; + break; + } + } + return rc ? rc : static_cast(target); +} + +int parsePreviewNumbers(Params::PreviewNumbers& previewNumbers, const std::string& optArg, int j) { + size_t k = j; + for (size_t i = j; i < optArg.size(); ++i) { + std::ostringstream os; + for (k = i; k < optArg.size() && isdigit(optArg[k]); ++k) { + os << optArg[k]; + } + if (k > i) { + bool ok = false; + auto num = Exiv2::stringTo(os.str(), ok); + if (ok && num >= 0) { + previewNumbers.insert(num); + } else { + std::cerr << Params::instance().progname() << ": " << _("Invalid preview number") << ": " << num << "\n"; + } + i = k; + } + if (k >= optArg.size() || optArg[i] != ',') + break; + } + auto ret = static_cast(k - j); + if (ret == 0) { + previewNumbers.insert(0); + } +#ifdef DEBUG + std::cout << "\nThe set now contains: "; + for (auto&& number : previewNumbers) { + std::cout << number << ", "; + } + std::cout << '\n'; +#endif + return static_cast(k - j); +} // parsePreviewNumbers + +bool parseCmdFiles(ModifyCmds& modifyCmds, const Params::CmdFiles& cmdFiles) { + for (auto&& filename : cmdFiles) { + try { + std::ifstream file(filename.c_str()); + bool bStdin = filename == "-"; + if (!file && !bStdin) { + std::cerr << filename << ": " << _("Failed to open command file for reading\n"); + return false; + } + int num = 0; + std::string line; + while (bStdin ? std::getline(std::cin, line) : std::getline(file, line)) { + ModifyCmd modifyCmd; + if (parseLine(modifyCmd, line, ++num)) { + modifyCmds.push_back(std::move(modifyCmd)); + } + } + } catch (const Exiv2::Error& error) { + std::cerr << filename << ", " << _("line") << " " << error << "\n"; + return false; + } + } + return true; +} // parseCmdFile + +bool parseCmdLines(ModifyCmds& modifyCmds, const Params::CmdLines& cmdLines) { + try { + int num = 0; + for (auto&& line : cmdLines) { + ModifyCmd modifyCmd; + if (parseLine(modifyCmd, line, ++num)) { + modifyCmds.push_back(std::move(modifyCmd)); + } + } + return true; + } catch (const Exiv2::Error& error) { + std::cerr << _("-M option") << " " << error << "\n"; + return false; + } +} // parseCmdLines + +#ifdef _WIN32 +std::string formatArg(const char* arg) { + std::string result; + char b = ' '; + char e = '\\'; + std::string E = std::string("\\"); + char q = '\''; + std::string Q = std::string("'"); + bool qt = false; + char* a = const_cast(arg); + while (*a) { + if (*a == b || *a == e || *a == q) + qt = true; + if (*a == q) + result += E; + if (*a == e) + result += E; + result += std::string(a, 1); + a++; + } + if (qt) + result = Q + result + Q; + + return result; +} +#endif + +bool parseLine(ModifyCmd& modifyCmd, const std::string& line, int num) { + const std::string delim = " \t"; + + // Skip empty lines and comments + std::string::size_type cmdStart = line.find_first_not_of(delim); + if (cmdStart == std::string::npos || line[cmdStart] == '#') + return false; + + // Get command and key + std::string::size_type cmdEnd = line.find_first_of(delim, cmdStart + 1); + std::string::size_type keyStart = line.find_first_not_of(delim, cmdEnd + 1); + std::string::size_type keyEnd = line.find_first_of(delim, keyStart + 1); + if (cmdEnd == std::string::npos || keyStart == std::string::npos) { + std::string cmdLine; +#ifdef _WIN32 + for (int i = 1; i < __argc; i++) { + cmdLine += std::string(" ") + formatArg(__argv[i]); + } +#endif + throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, + std::to_string(num) + ": " + _("Invalid command line:") + cmdLine); + } + + std::string cmd(line.substr(cmdStart, cmdEnd - cmdStart)); + CmdId cmdId = commandId(cmd); + if (cmdId == CmdId::invalid) { + throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, + std::to_string(num) + ": " + _("Invalid command") + " `" + cmd + "'"); + } + + Exiv2::TypeId defaultType = Exiv2::invalidTypeId; + std::string key(line.substr(keyStart, keyEnd - keyStart)); + MetadataId metadataId = MetadataId::invalid; + if (cmdId != CmdId::reg) { + try { + Exiv2::IptcKey iptcKey(key); + metadataId = MetadataId::iptc; + defaultType = Exiv2::IptcDataSets::dataSetType(iptcKey.tag(), iptcKey.record()); + } catch (const Exiv2::Error&) { + } + if (metadataId == MetadataId::invalid) { + try { + Exiv2::ExifKey exifKey(key); + metadataId = MetadataId::exif; + defaultType = exifKey.defaultTypeId(); + } catch (const Exiv2::Error&) { + } + } + if (metadataId == MetadataId::invalid) { + try { + Exiv2::XmpKey xmpKey(key); + metadataId = MetadataId::xmp; + defaultType = Exiv2::XmpProperties::propertyType(xmpKey); + } catch (const Exiv2::Error&) { + } + } + if (metadataId == MetadataId::invalid) { + throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, + std::to_string(num) + ": " + _("Invalid key") + " `" + key + "'"); + } + } + std::string value; + Exiv2::TypeId type = defaultType; + bool explicitType = false; + if (cmdId != CmdId::del) { + // Get type and value + std::string::size_type typeStart = std::string::npos; + if (keyEnd != std::string::npos) + typeStart = line.find_first_not_of(delim, keyEnd + 1); + std::string::size_type typeEnd = std::string::npos; + if (typeStart != std::string::npos) + typeEnd = line.find_first_of(delim, typeStart + 1); + std::string::size_type valStart = typeStart; + std::string::size_type valEnd = std::string::npos; + if (valStart != std::string::npos) + valEnd = line.find_last_not_of(delim); + + if (cmdId == CmdId::reg && (keyEnd == std::string::npos || valStart == std::string::npos)) { + throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, + std::to_string(num) + ": " + _("Invalid command line") + " "); + } + + if (cmdId != CmdId::reg && typeStart != std::string::npos && typeEnd != std::string::npos) { + std::string typeStr(line.substr(typeStart, typeEnd - typeStart)); + Exiv2::TypeId tmpType = Exiv2::TypeInfo::typeId(typeStr); + if (tmpType != Exiv2::invalidTypeId) { + valStart = line.find_first_not_of(delim, typeEnd + 1); + if (valStart == std::string::npos) { + throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, + std::to_string(num) + ": " + _("Invalid command line") + " "); + } + type = tmpType; + explicitType = true; + } + } + + if (valStart != std::string::npos) { + value = parseEscapes(line.substr(valStart, valEnd + 1 - valStart)); + if ((value.front() == '"' && value.back() == '"') || (value.front() == '\'' && value.back() == '\'')) { + value = value.substr(1, value.length() - 2); + } + } + } + + modifyCmd.cmdId_ = cmdId; + modifyCmd.key_ = key; + modifyCmd.metadataId_ = metadataId; + modifyCmd.typeId_ = type; + modifyCmd.explicitType_ = explicitType; + modifyCmd.value_ = value; + + if (cmdId == CmdId::reg) { + if (value.empty()) { + throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, + std::to_string(num) + ": " + _("Empty value for key") + +" `" + key + "'"); + } + + // Registration needs to be done immediately as the new namespaces are + // looked up during parsing of subsequent lines (to validate XMP keys). + Exiv2::XmpProperties::registerNs(modifyCmd.value_, modifyCmd.key_); + } + + return true; +} // parseLine + +CmdId commandId(const std::string& cmdString) { + if (auto it = Exiv2::find(cmdIdAndString, cmdString)) + return it->cmdId_; + return CmdId::invalid; +} + +std::string parseEscapes(const std::string& input) { + std::string result; + for (size_t i = 0; i < input.length(); ++i) { + char ch = input[i]; + if (ch != '\\') { + result.push_back(ch); + continue; + } + size_t escapeStart = i; + if (input.length() - 1 <= i) { + result.push_back(ch); + continue; + } + ++i; + ch = input[i]; + switch (ch) { + case '\\': // Escaping of backslash + result.push_back('\\'); + break; + case 'r': // Escaping of carriage return + result.push_back('\r'); + break; + case 'n': // Escaping of newline + result.push_back('\n'); + break; + case 't': // Escaping of tab + result.push_back('\t'); + break; + case 'u': // Escaping of unicode + if (input.length() >= 4 && input.length() - 4 > i) { + uint32_t acc = 0; + for (int j = 0; j < 4; ++j) { + ++i; + acc <<= 4; + if (input[i] >= '0' && input[i] <= '9') { + acc |= input[i] - '0'; + } else if (input[i] >= 'a' && input[i] <= 'f') { + acc |= input[i] - 'a' + 10; + } else if (input[i] >= 'A' && input[i] <= 'F') { + acc |= input[i] - 'A' + 10; + } else { + acc = 0xFFFFFFFF; + break; + } + } + if (acc == 0xFFFFFFFF) { + result.push_back('\\'); + i = escapeStart; + break; + } + + std::string ucs2toUtf8; + ucs2toUtf8.push_back((acc & 0xff00U) >> 8); + ucs2toUtf8.push_back(acc & 0x00ffU); + + if (Exiv2::convertStringCharset(ucs2toUtf8, "UCS-2BE", "UTF-8")) { + result.append(ucs2toUtf8); + } + } else { + result.push_back('\\'); + result.push_back(ch); + } + break; + default: + result.push_back('\\'); + result.push_back(ch); + } + } + return result; +} + +} // namespace diff --git a/app/exiv2app.hpp b/app/exiv2app.hpp new file mode 100644 index 0000000000..6fbfb38abe --- /dev/null +++ b/app/exiv2app.hpp @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/*! + @brief Defines class Params, used for the command line handling of exiv2 + @author Andreas Huggel (ahu) + ahuggel@gmx.net + @date 08-Dec-03, ahu: created + */ +#ifndef EXIV2APP_HPP_ +#define EXIV2APP_HPP_ + +// ***************************************************************************** +// included header files +#include + +#include "getopt.hpp" + +// + standard includes +#include +#include +#include + +//! Command identifiers +enum class CmdId { + invalid, + add, + set, + del, + reg, +}; +//! Metadata identifiers +enum class MetadataId : uint32_t { + invalid = Exiv2::mdNone, // 0 + exif = Exiv2::mdExif, // 1 + iptc = Exiv2::mdIptc, // 2 + xmp = Exiv2::mdXmp, // 8 +}; + +inline MetadataId operator&(MetadataId x, MetadataId y) { + return static_cast(static_cast(x) & static_cast(y)); +} + +inline MetadataId operator|(MetadataId x, MetadataId y) { + return static_cast(static_cast(x) | static_cast(y)); +} + +inline MetadataId& operator|=(MetadataId& x, MetadataId y) { + return x = x | y; +} + +//! Structure for one parsed modification command +struct ModifyCmd { + //! C'tor + ModifyCmd() = default; + CmdId cmdId_{CmdId::invalid}; //!< Command identifier + std::string key_; //!< Exiv2 key string + MetadataId metadataId_{MetadataId::invalid}; //!< Metadata identifier + Exiv2::TypeId typeId_{Exiv2::invalidTypeId}; //!< Exiv2 type identifier + //! Flag to indicate if the type was explicitly specified (true) + bool explicitType_{false}; + std::string value_; //!< Data +}; +//! Container for modification commands +using ModifyCmds = std::vector; +/*! + @brief Implements the command line handling for the program. + + Derives from Util::Getopt to use the command line argument parsing + functionality provided there. This class is implemented as a singleton, + i.e., there is only one global instance of it, which can be accessed + from everywhere. + + Usage example:
+ @code + #include "params.h" + + int main(int argc, char* const argv[]) + { + Params& params = Params::instance(); + if (params.getopt(argc, argv)) { + params.usage(); + return 1; + } + if (params.help_) { + params.help(); + return 0; + } + if (params.version_) { + params.version(); + return 0; + } + + // do something useful here... + + return 0; + } + @endcode + */ + +class Params : public Util::Getopt { + private: + std::string optstring_; + + public: + //! Container for command files + using CmdFiles = std::vector; + //! Container for commands from the command line + using CmdLines = std::vector; + //! Container to store filenames. + using Files = std::vector; + //! Container for preview image numbers + using PreviewNumbers = std::set; + //! Container for keys + using Keys = std::vector; + + /*! + @brief Controls all access to the global Params instance. + @return Reference to the global Params instance. + */ + static Params& instance(); + + //! Prevent copy-construction: not implemented. + ~Params() override = default; + Params(const Params&) = delete; + Params& operator=(const Params&) = delete; + + //! Enumerates print modes + enum PrintMode { + pmSummary, + pmList, + pmComment, + pmPreview, + pmStructure, + pmXMP, + pmIccProfile, + pmRecursive, + }; + + //! Individual items to print, bitmap + enum PrintItem : uint32_t { + prTag = 1, + prGroup = 2, + prKey = 4, + prName = 8, + prLabel = 16, + prType = 32, + prCount = 64, + prSize = 128, + prValue = 256, + prTrans = 512, + prHex = 1024, + prSet = 2048, + prDesc = 4096 + }; + + //! Enumerates common targets, bitmap + enum CommonTarget : uint32_t { + ctExif = 1, + ctIptc = 2, + ctComment = 4, + ctThumb = 8, + ctXmp = 16, + ctXmpSidecar = 32, + ctPreview = 64, + ctIccProfile = 128, + ctXmpRaw = 256, + ctStdInOut = 512, + ctIptcRaw = 1024 + }; + + //! Enumerates the policies to handle existing files in rename action + enum FileExistsPolicy { + overwritePolicy, + renamePolicy, + askPolicy, + }; + + //! Enumerates year, month and day adjustments. + enum Yod { + yodYear, + yodMonth, + yodDay, + }; + + //! Structure for year, month and day adjustment command line arguments. + struct YodAdjust { + bool flag_; //!< Adjustment flag. + const char* option_; //!< Adjustment option string. + int64_t adjustment_; //!< Adjustment value. + }; + + bool help_{false}; //!< Help option flag. + bool version_{false}; //!< Version option flag. + bool verbose_{false}; //!< Verbose (talkative) option flag. + bool force_{false}; //!< Force overwrites flag. + bool binary_{false}; //!< Suppress long binary values. + bool unknown_{true}; //!< Suppress unknown tags. + bool preserve_{false}; //!< Preserve timestamps flag. + bool timestamp_{false}; //!< Rename also sets the file timestamp. + bool timestampOnly_{false}; //!< Rename only sets the file timestamp. + FileExistsPolicy fileExistsPolicy_{askPolicy}; //!< What to do if file to rename exists. + bool adjust_{false}; //!< Adjustment flag. + PrintMode printMode_{pmSummary}; //!< Print mode. + PrintItem printItems_{0}; //!< Print items. + MetadataId printTags_{Exiv2::mdNone}; //!< Print tags (bitmap of MetadataId flags). + //! %Action (integer rather than TaskType to avoid dependency). + int action_{0}; + CommonTarget target_; //!< What common target to process. + + int64_t adjustment_{0}; //!< Adjustment in seconds. + std::array yodAdjust_; //!< Year, month and day adjustment info. + std::string format_; //!< Filename format (-r option arg). + bool formatSet_{false}; //!< Whether the format is set with -r + CmdFiles cmdFiles_; //!< Names of the modification command files + CmdLines cmdLines_; //!< Commands from the command line + ModifyCmds modifyCmds_; //!< Parsed modification commands + std::string jpegComment_; //!< Jpeg comment to set in the image + std::string directory_; //!< Location for files to extract/insert + std::string suffix_; //!< File extension of the file to insert + Files files_; //!< List of non-option arguments. + PreviewNumbers previewNumbers_; //!< List of preview numbers + std::vector greps_; //!< List of keys to 'grep' from the metadata + Keys keys_; //!< List of keys to match from the metadata + std::string charset_; //!< Charset to use for UNICODE Exif user comment + + Exiv2::DataBuf stdinBuf; //!< DataBuf with the binary bytes from stdin + + private: + bool first_{true}; + + Params(); + + //! @name Helpers + //@{ + int setLogLevel(const std::string& optarg); + int evalGrep(const std::string& optarg); + int evalKey(const std::string& optarg); + int evalRename(int opt, const std::string& optarg); + int evalAdjust(const std::string& optarg); + int evalYodAdjust(const Yod& yod, const std::string& optarg); + int evalPrint(const std::string& optarg); + int evalPrintFlags(const std::string& optarg); + int evalDelete(const std::string& optarg); + int evalExtract(const std::string& optarg); + int evalInsert(const std::string& optarg); + int evalModify(int opt, const std::string& optarg); + //@} + + public: + /*! + @brief Call Getopt::getopt() with optstring, to initiate command line + argument parsing, perform consistency checks after all command line + arguments are parsed. + + @param argc Argument count as passed to main() on program invocation. + @param argv Argument array as passed to main() on program invocation. + + @return 0 if successful, >0 in case of errors. + */ + int getopt(int argc, char* const argv[]); + + //! Handle options and their arguments. + int option(int opt, const std::string& optarg, int optopt) override; + + //! Handle non-option parameters. + int nonoption(const std::string& argv) override; + + //! Print a minimal usage note to an output stream. + void usage(std::ostream& os = std::cout) const; + + //! Print further usage explanations to an output stream. + void help(std::ostream& os = std::cout) const; + + //! Print version information to an output stream. + static void version(bool verbose = false, std::ostream& os = std::cout); + + //! getStdin binary data read from stdin to DataBuf + /* + stdin can be used by multiple images in the exiv2 command line: + For example: $ cat foo.icc | exiv2 -iC- a.jpg b.jpg c.jpg will modify the ICC profile in several images. + */ + void getStdin(Exiv2::DataBuf& buf); + +}; // class Params + +inline Params::CommonTarget operator|(Params::CommonTarget x, Params::CommonTarget y) { + return static_cast(static_cast(x) | static_cast(y)); +} + +inline Params::CommonTarget& operator|=(Params::CommonTarget& x, Params::CommonTarget y) { + return x = x | y; +} + +inline Params::PrintItem operator|(Params::PrintItem x, Params::PrintItem y) { + return static_cast(static_cast(x) | static_cast(y)); +} + +inline Params::PrintItem& operator|=(Params::PrintItem& x, Params::PrintItem y) { + return x = x | y; +} + +#endif // #ifndef EXIV2APP_HPP_ diff --git a/app/getopt.cpp b/app/getopt.cpp new file mode 100644 index 0000000000..935fd77d63 --- /dev/null +++ b/app/getopt.cpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +// included header files +#include "getopt.hpp" + +#include +#include +#include + +namespace fs = std::filesystem; + +namespace Util { +// https://raw.githubusercontent.com/skeeto/getopt/master/getopt.h +int optind = 0; +int opterr = 1; +int optopt; +int optpos = 1; +const char* optarg; + +/* A minimal POSIX getopt() implementation in ANSI C + * + * This is free and unencumbered software released into the public domain. + * + * This implementation supports the convention of resetting the option + * parser by assigning optind to 0. This resets the internal state + * appropriately. + * + * Ref: http://pubs.opengroup.org/onlinepubs/9699919799/functions/getopt.html + */ + +int getopt(int argc, char* const argv[], const char* optstring) { + const char* arg; + (void)argc; + + /* Reset? */ + if (optind == 0) { + optind = 1; + optpos = 1; + } + + arg = argv[optind]; + if (arg && strcmp(arg, "--") == 0) { + optind++; + return -1; + } + if (!arg || arg[0] != '-' || !isalnum(arg[1])) { + return -1; + } + const char* opt = strchr(optstring, arg[optpos]); + optopt = arg[optpos]; + if (!opt) { + if (opterr && *optstring != ':') + fprintf(stderr, "%s: illegal option: %c\n", argv[0], optopt); + return '?'; + } + if (opt[1] == ':') { + if (arg[optpos + 1]) { + optarg = const_cast(arg) + optpos + 1; + optind++; + optpos = 1; + return optopt; + } + if (argv[optind + 1]) { + optarg = argv[optind + 1]; + optind += 2; + optpos = 1; + return optopt; + } + if (opterr && *optstring != ':') + fprintf(stderr, "%s: option requires an argument: %c\n", argv[0], optopt); + return *optstring == ':' ? ':' : '?'; + } + if (!arg[++optpos]) { + optind++; + optpos = 1; + } + return optopt; +} + +// ***************************************************************************** +// class Getopt +int Getopt::getopt(int argc, char* const argv[], const std::string& optstring) { + progname_ = fs::path(argv[0]).filename().string(); + Util::optind = 0; // reset the Util::Getopt scanner + + while (!errcnt_) { + int c = Util::getopt(argc, argv, optstring.c_str()); + if (c == -1) { + break; + } + errcnt_ += option(c, Util::optarg ? Util::optarg : "", Util::optopt); + if (c == '?') { + break; + } + } + for (int i = Util::optind; i < argc; i++) { + errcnt_ += nonoption(argv[i]); + } + + return errcnt_; +} + +int Getopt::nonoption(const std::string& /*argv*/) { + return 0; +} + +} // namespace Util diff --git a/app/getopt.hpp b/app/getopt.hpp new file mode 100644 index 0000000000..3b8da8d1eb --- /dev/null +++ b/app/getopt.hpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef GETOPT_H +#define GETOPT_H + +#include + +namespace Util { +extern int optind; +extern int opterr; +extern int optopt; +extern int optpos; +extern const char* optarg; + +int getopt(int argc, char* const argv[], const char* optstring); + +// ********************************************************************* +// class definitions + +/*! + @brief Parse the command line options of a program. + + A wrapper around the POSIX %getopt(3) function. Parses the command line + options and passes each option to virtual option(). A derived class + implements this method to handle options as needed. Similarly, + remaining non-option parameters are passed to the virtual nonoption() + method. + */ +class Getopt { + public: + //! Default constructor. + Getopt() = default; + + //! Destructor. + virtual ~Getopt() = default; + + /*! + @brief Parse command line arguments. + + Parses the command line arguments. Calls option() with the + character value of the option and its argument (if any) for each + recognized option and with ':' or '?' for unrecognized options. + See the manual pages for %getopt(3) for details. In addition, + nonoption() is invoked for each remaining non-option parameter on + the command line. + + @param argc Argument count as passed to main() on program invocation. + @param argv Argument array as passed to main() on program invocation. + @param optstring String containing the legitimate option characters. + + @return Number of errors (the sum of the return values from option() + and nonoption()). + */ + int getopt(int argc, char* const argv[], const std::string& optstring); + + /*! + @brief Callback used by getopt() to pass on each option and its + argument (if any). + + Implement this method in a derived class to handle the options as + needed. See the manual pages for %getopt(3) for further details, in + particular, the semantics of optarg and optopt. + + @param opt Value of the option character as returned by %getopt(3). + @param optarg The corresponding option argument. + @param optopt The actual option character in case of an unrecognized + option or a missing option argument (opt is '?' or ':'). + + @return 0 if successful, 1 in case of an error. + */ + virtual int option(int opt, const std::string& optarg, int optopt) = 0; + + /*! + @brief Callback used by getopt() to pass on each non-option parameter + found on the command line. + + Implement this method in a derived class to handle the non-option + parameters as needed. The default implementation ignores all non-option + parameters. + + @param argv The non-option parameter from the command line. + + @return 0 if successful, 1 in case of an error. + */ + virtual int nonoption(const std::string& argv); + + //! Program name (argv[0]) + [[nodiscard]] const std::string& progname() const { + return progname_; + } + + //! Total number of errors returned by calls to option() + [[nodiscard]] int errcnt() const { + return errcnt_; + } + + private: + std::string progname_; + int errcnt_{0}; +}; + +} // namespace Util + +#endif diff --git a/app/utf8.manifest b/app/utf8.manifest new file mode 100644 index 0000000000..f205604931 --- /dev/null +++ b/app/utf8.manifest @@ -0,0 +1,8 @@ + + + + + UTF-8 + + + \ No newline at end of file diff --git a/app/utf8.rc b/app/utf8.rc new file mode 100644 index 0000000000..b4cca2abbe --- /dev/null +++ b/app/utf8.rc @@ -0,0 +1,3 @@ +#include + +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "utf8.manifest" diff --git a/ci/backup/appveyor.yml b/ci/backup/appveyor.yml new file mode 100644 index 0000000000..7a15ba339c --- /dev/null +++ b/ci/backup/appveyor.yml @@ -0,0 +1,69 @@ +# Configuration rescued from 'old-master' branch. +# Only one Visual Studio (latest one), and all possible configurations +image: Visual Studio 2019 + +configuration: + - Debug + - Release + +platform: + - x86 + - x64 + +environment: + VCVARS: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat + + PYTHON: "C:\\Python39-x64" + + matrix: + - SHARED: ON + - SHARED: OFF + +shallow_clone: true + +install: + - set PATH=%PATH%;%PYTHON%/Scripts/ + - echo %APPVEYOR_BUILD_FOLDER% + - if not exist deps mkdir deps + - cd deps + - if not exist ninja appveyor DownloadFile https://github.com/ninja-build/ninja/releases/download/v1.10.2/ninja-win.zip -FileName ninja.zip + - if not exist ninja 7z x ninja.zip -o%APPVEYOR_BUILD_FOLDER%\deps\ninja > nul + - set PATH=%APPVEYOR_BUILD_FOLDER%\deps\ninja;%PATH% + - ninja --version + - python -m pip install --upgrade pip + - pip3.exe install conan==1.35.2 + - pip3.exe install lxml + - cd %APPVEYOR_BUILD_FOLDER% + +before_build: + - cmd: conan remote list + - cmd: conan config set storage.path=c:\Users\appveyor\conanCache + - cmd: conan profile new --detect default + - cmd: conan profile update settings.compiler.version=16 default + - cmd: if "%platform%"=="x64" (set ARCHITECTURE=x86_64) else (set ARCHITECTURE=x86) + - cmd: if "%CONFIGURATION%"=="Debug" (set RUNTIME=MDd) else (set RUNTIME=MD) + - cmd: conan profile update settings.arch=%ARCHITECTURE% default + - cmd: conan profile update settings.arch_build=%ARCHITECTURE% default + - cmd: conan profile update settings.build_type=%CONFIGURATION% default + - cmd: conan profile update settings.compiler.runtime=%RUNTIME% default + - cmd: cat c:\Users\appveyor\.conan\conan.conf + - cmd: cat c:\Users\appveyor\.conan\profiles\default + +build_script: + - cmd: md build + - cmd: cd build + - cmd: if "%platform%"=="x64" (set VC_ARCH=x86_amd64) else (set VC_ARCH=x86) + - cmd: call "%VCVARS%" %VC_ARCH% + - cmd: conan install .. --build missing -o webready=True + - cmd: cmake -GNinja -DBUILD_SHARED_LIBS=%SHARED% -DEXIV2_TEAM_WARNINGS_AS_ERRORS=ON -DCMAKE_BUILD_TYPE=%CONFIGURATION% -DEXIV2_ENABLE_NLS=OFF -DEXIV2_ENABLE_PNG=ON -DEXIV2_ENABLE_WEBREADY=ON -DEXIV2_ENABLE_CURL=ON -DEXIV2_BUILD_UNIT_TESTS=ON -DEXIV2_ENABLE_WIN_UNICODE=OFF -DEXIV2_ENABLE_BMFF=ON -DCMAKE_INSTALL_PREFIX=install .. + - cmd: ninja + - cmd: ninja install + - cmd: cd bin + - cmd: unit_tests.exe + - cmd: cd ../../tests/ + - cmd: set EXIV2_EXT=.exe + - cmd: c:\Python39-x64\python.exe runner.py -v + +cache: +- deps # Ninja installation + #- c:\Users\appveyor\conanCache # Conan cache diff --git a/appveyor.yml b/ci/backup/appveyor_all_vs_versions.yml similarity index 81% rename from appveyor.yml rename to ci/backup/appveyor_all_vs_versions.yml index 29f4ce68ba..796cb9afb8 100644 --- a/appveyor.yml +++ b/ci/backup/appveyor_all_vs_versions.yml @@ -12,7 +12,6 @@ environment: VCVARS: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat ARCHITECTURE: x86_64 UNIT_TESTS: 1 - WEBREADY: False WARNINGS_AS_ERRORS: ON - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 CMAKE_GENERATOR: Ninja @@ -21,7 +20,6 @@ environment: VCVARS: C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat ARCHITECTURE: x86_64 UNIT_TESTS: 1 - WEBREADY: False WARNINGS_AS_ERRORS: ON - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 CMAKE_GENERATOR: Ninja @@ -30,17 +28,7 @@ environment: VCVARS: C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat ARCHITECTURE: x86_64 UNIT_TESTS: 1 - WEBREADY: False WARNINGS_AS_ERRORS: ON - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2013 - CMAKE_GENERATOR: Ninja - INTEGRATION_TESTS: 0 - VS_COMPILER_VERSION: 12 - VCVARS: C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat - ARCHITECTURE: x86_64 - UNIT_TESTS: 0 - WEBREADY: False - WARNINGS_AS_ERRORS: OFF shallow_clone: true @@ -54,11 +42,12 @@ install: - set PATH=C:\projects\deps\ninja;%PATH% - ninja --version - python -m pip install --upgrade pip - - pip3.exe install conan==1.30.2 + - pip3.exe install conan==1.35.2 - pip3.exe install lxml - cd %APPVEYOR_BUILD_FOLDER% before_build: + - cmd: conan config install https://github.com/conan-io/conanclientcert.git - cmd: conan remote list - cmd: conan config set storage.path=c:\Users\appveyor\conanCache - cmd: conan profile new --detect default @@ -72,9 +61,9 @@ build_script: - cmd: cd build - cmd: call "%VCVARS%" x86_amd64 - cmd: conan --version - - cmd: conan install .. -o webready=%WEBREADY% --build missing + - cmd: conan install .. -o webready=False --build missing - cmd: echo %CMAKE_GENERATOR% - - cmd: cmake -G "%CMAKE_GENERATOR%" -DEXIV2_TEAM_WARNINGS_AS_ERRORS=%WARNINGS_AS_ERRORS% -DCMAKE_BUILD_TYPE=Release -DEXIV2_ENABLE_NLS=OFF -DEXIV2_ENABLE_PNG=ON -DEXIV2_ENABLE_BMFF=ON -DEXIV2_ENABLE_WEBREADY=%WEBREADY% -DEXIV2_BUILD_UNIT_TESTS=%UNIT_TESTS% -DCMAKE_INSTALL_PREFIX=install .. + - cmd: cmake -G "%CMAKE_GENERATOR%" -DEXIV2_TEAM_WARNINGS_AS_ERRORS=%WARNINGS_AS_ERRORS% -DCMAKE_BUILD_TYPE=Release -DEXIV2_ENABLE_NLS=OFF -DEXIV2_ENABLE_PNG=ON -DEXIV2_ENABLE_BMFF=ON -DEXIV2_ENABLE_WEBREADY=OFF -DEXIV2_BUILD_UNIT_TESTS=%UNIT_TESTS% -DCMAKE_INSTALL_PREFIX=install .. - cmd: cmake --build . --config Release - cmd: cmake --build . --config Release --target install - cmd: cd bin diff --git a/appveyor_mingw_cygwin.yml b/ci/backup/appveyor_mingw_cygwin.yml similarity index 90% rename from appveyor_mingw_cygwin.yml rename to ci/backup/appveyor_mingw_cygwin.yml index 31f054d4b4..5fce258b3c 100644 --- a/appveyor_mingw_cygwin.yml +++ b/ci/backup/appveyor_mingw_cygwin.yml @@ -11,14 +11,14 @@ environment: ARCHITECTURE: x86_64 UNIT_TESTS: 1 WEBREADY: False - WARNINGS_AS_ERRORS: ON + WARNINGS_AS_ERRORS: OFF - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 BUILD: CYGWIN64 INTEGRATION_TESTS: 1 ARCHITECTURE: x86_64 UNIT_TESTS: 1 WEBREADY: False - WARNINGS_AS_ERRORS: ON + WARNINGS_AS_ERRORS: OFF shallow_clone: true @@ -39,7 +39,7 @@ install: build_script: - cmd: set CMD=mkdir -p build - cmd: set CMD=%CMD%; cd build - - cmd: set CMD=%CMD%; cmake .. -G 'Unix Makefiles' -DCMAKE_CXX_STANDARD=98 -DCMAKE_CXX_FLAGS=-Wno-deprecated + - cmd: set CMD=%CMD%; cmake .. -G 'Unix Makefiles' - cmd: set CMD=%CMD%; cmake --build . --config Release - cmd: rem echo %CMD% - cd %APPVEYOR_BUILD_FOLDER% @@ -47,7 +47,7 @@ build_script: - cmd: set CMD=which python3 python - cmd: set CMD=%CMD%; python --version - cmd: set CMD=%CMD%; build/bin/exiv2 --verbose --version; pwd ; ls -l - - cmd: set CMD=%CMD%; cd build ; cmake --build . --config Release --target python_tests + - cmd: set CMD=%CMD%; cd build ; cmake --build . --config Release --target python_tests - cmd: echo %CMD% - cd %APPVEYOR_BUILD_FOLDER% - cmd: if "%BUILD%"=="MINGW64" C:\msys64\usr\bin\bash -c "%CMD%" @@ -55,7 +55,7 @@ build_script: - cmd: set CMD=rm -rf build - cmd: set CMD=%CMD%; mkdir -p build - cmd: set CMD=%CMD%; cd build - - cmd: set CMD=%CMD%;cmake .. -DCMAKE_CXX_STANDARD=98 -DCMAKE_CXX_FLAGS=-Wno-deprecated + - cmd: set CMD=%CMD%;cmake .. - cmd: set CMD=%CMD%; make - cmd: set CMD=%CMD%; make python_tests - cmd: echo %CMD% diff --git a/ci/install.sh b/ci/install.sh deleted file mode 100755 index 6d520d27bd..0000000000 --- a/ci/install.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -set -e # Enables cheking of return values from each command -set -x # Prints every command - -# This file is only used from Travis CI, where the only Linux distro used is Ubuntu - -python --version -python3 --version - -if [[ "$(uname -s)" == 'Linux' ]]; then - sudo apt-get update - - sudo apt-get install cmake - if [[ "$(lsb_release -cs)" == 'focal' ]]; then - # In Ubuntu 20.04 python-pip does not exist. Furthermore we need to have the alias python for python3 - sudo apt-get install zlib1g-dev libssh-dev python3-pip python-is-python3 libxml2-utils - else - sudo apt-get install zlib1g-dev libssh-dev python-pip libxml2-utils - fi - - if [ -n "$WITH_VALGRIND" ]; then - sudo apt-get install valgrind - fi - sudo pip install virtualenv - virtualenv conan - source conan/bin/activate - pip install conan==1.30.2 - pip install codecov - pip install lxml -else - sudo pip3 install virtualenv - virtualenv conan - source conan/bin/activate - pip3 install conan==1.30.2 - pip3 install codecov - pip3 install lxml -fi - -conan --version -conan config set storage.path=~/conanData -conan profile new default --detect - -if [[ "$(uname -s)" == 'Linux' ]]; then - conan profile update settings.compiler.libcxx=libstdc++11 default -fi - diff --git a/ci/install_dependencies.sh b/ci/install_dependencies.sh index 396319d92d..745578173e 100755 --- a/ci/install_dependencies.sh +++ b/ci/install_dependencies.sh @@ -8,64 +8,65 @@ debian_build_gtest() { [ -d gtest_build ] || mkdir gtest_build cd gtest_build - cmake -DBUILD_SHARED_LIBS=1 /usr/src/googletest/googletest - make - cp libgtest* /usr/lib/ + cmake -GNinja -DBUILD_SHARED_LIBS=1 /usr/src/googletest/googletest + cmake --build . + if [ -f "lib/libgtest.so" ]; then + # Ubuntu 20.04 with gtest 1.10 + cp lib/libgtest* /usr/lib/ + else + # Debian 9 with gtest 1.8 + cp libgtest* /usr/lib/ + fi cd .. } -# workaround for really bare-bones Archlinux containers: -if [ -x "$(command -v pacman)" ]; then - pacman --noconfirm -Sy - pacman --noconfirm -S grep gawk sed -fi +# workaround for really old bare-bones Archlinux containers: +# if [ -x "$(command -v pacman)" ]; then +# pacman --noconfirm -Sy +# pacman --noconfirm --needed -S grep gawk sed +# fi distro_id=$(grep '^ID=' /etc/os-release|awk -F = '{print $2}'|sed 's/\"//g') case "$distro_id" in 'fedora') - dnf -y --refresh install gcc-c++ clang cmake make ccache expat-devel zlib-devel libssh-devel libcurl-devel gtest-devel which dos2unix glibc-langpack-en diffutils + dnf -y --refresh install gcc-c++ clang cmake ninja-build expat-devel zlib-devel brotli-devel libssh-devel libcurl-devel inih-devel gmock-devel glibc-langpack-en ;; 'debian') apt-get update - apt-get install -y cmake g++ clang make ccache python3 libexpat1-dev zlib1g-dev libssh-dev libcurl4-openssl-dev libgtest-dev libxml2-utils - debian_build_gtest + apt-get install -y cmake ninja-build g++ clang libexpat1-dev zlib1g-dev libbrotli-dev libssh-dev libcurl4-openssl-dev libgmock-dev libxml2-utils libinih-dev + # debian_build_gtest ;; 'arch') pacman --noconfirm -Syu - pacman --noconfirm -S gcc clang cmake make ccache expat zlib libssh curl gtest python dos2unix which diffutils + pacman --noconfirm --needed -S gcc clang cmake ninja expat zlib brotli libssh curl gtest libinih ;; 'ubuntu') apt-get update - apt-get install -y cmake g++ clang make ccache python3 libexpat1-dev zlib1g-dev libssh-dev libcurl4-openssl-dev libgtest-dev google-mock libxml2-utils - debian_build_gtest + apt-get install -y cmake ninja-build g++ clang libexpat1-dev zlib1g-dev libbrotli-dev libssh-dev libcurl4-openssl-dev libgmock-dev libxml2-utils libinih-dev + # debian_build_gtest ;; - 'centos'|'rhel') - yum -y install epel-release - # enable copr for gtest - curl https://copr.fedorainfracloud.org/coprs/defolos/devel/repo/epel-7/defolos-devel-epel-7.repo > /etc/yum.repos.d/_copr_defolos-devel.repo - yum clean all - yum -y install gcc-c++ clang cmake3 make ccache expat-devel zlib-devel libssh-devel libcurl-devel gtest-devel which python3 dos2unix - # symlink up to date versions of cmake to 'default' name - mv /bin/cmake /bin/.cmake.old - ln -s /bin/cmake3 /bin/cmake + 'alpine') + apk update + apk add gcc g++ clang cmake samurai expat-dev zlib-dev brotli-dev libssh-dev curl-dev gtest gtest-dev gmock libintl gettext-dev libxml2-utils inih-dev inih-inireader-dev ;; - 'opensuse'|'opensuse-tumbleweed') + 'rhel') + dnf -y --refresh install gcc-c++ clang cmake ninja-build expat-devel zlib-devel brotli-devel libssh-devel libcurl-devel inih-devel + ;; + + 'centos') + dnf -y --refresh install gcc-c++ clang cmake expat-devel zlib-devel brotli-devel libssh-devel libcurl-devel + dnf -y --refresh --enablerepo=crb install ninja-build inih-devel + ;; + + 'opensuse-tumbleweed') zypper --non-interactive refresh - zypper --non-interactive install gcc-c++ clang cmake make ccache libexpat-devel zlib-devel libssh-devel curl tar libcurl-devel git which dos2unix libxml2-tools - pushd /tmp - curl -LO https://github.com/google/googletest/archive/release-1.8.0.tar.gz - tar xzf release-1.8.0.tar.gz - mkdir -p googletest-release-1.8.0/build - pushd googletest-release-1.8.0/build - cmake .. ; make ; make install - popd - popd + zypper --non-interactive install gcc-c++ clang cmake ninja libexpat-devel zlib-devel libbrotli-devel libssh-devel libcurl-devel gmock libxml2-tools libinih-devel ;; *) echo "Sorry, no predefined dependencies for your distribution $distro_id exist yet" diff --git a/ci/run.sh b/ci/run.sh deleted file mode 100755 index b6da9ffe2c..0000000000 --- a/ci/run.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -set -e -set -x - -source conan/bin/activate - -if [[ "$(uname -s)" == 'Linux' ]]; then - if [ "$CC" == "clang" ]; then - # clang + Ubuntu don't like to run with UBSAN, but ASAN works - export CMAKE_OPTIONS="$COMMON_CMAKE_OPTIONS" - elif [ -n "$WITH_VALGRIND" ]; then - export EXIV2_VALGRIND="valgrind --quiet" - else - export CMAKE_OPTIONS="$COMMON_CMAKE_OPTIONS -DEXIV2_TEAM_USE_SANITIZERS=OFF" - fi -else - export CMAKE_OPTIONS="$COMMON_CMAKE_OPTIONS -DEXIV2_TEAM_USE_SANITIZERS=OFF" -fi -CMAKE_OPTIONS="$COMMON_CMAKE_OPTIONS -DCMAKE_CXX_FLAGS=-Wno-deprecated -DCMAKE_CXX_STANDARD=98 -DCMAKE_BUILD_TYPE=$BUILD_TYPE" - -mkdir build -cd build -conan install .. -o webready=True --build missing -cmake ${CMAKE_OPTIONS} .. -make -j -make tests -make install - -# Check for detecting issues with the installation of headers -if [ `ls install/include/exiv2/ | wc -l` > 10 ]; then - echo Headers installed correctly -else - echo There was some problem with the installation of the public headers - exit 1 -fi - -if [ -n "$COVERAGE" ]; then - bash <(curl -s https://codecov.io/bash) -fi - diff --git a/ci/test_build.py b/ci/test_build.py index 9faaafc912..2fabf6d55c 100644 --- a/ci/test_build.py +++ b/ci/test_build.py @@ -34,7 +34,7 @@ def matrix_build(shared_libs, ccs, build_types, cmake_bin, cmake_options, os.path.join( "build", "_".join( - map(lambda p: str(p) if p is not None else "", params) + map(str, filter(None, params)) ) ) ) @@ -46,7 +46,7 @@ def matrix_build(shared_libs, ccs, build_types, cmake_bin, cmake_options, cmake_options, cmake_bin=cmake_bin, build_type=build_type, lib_type=lib_type, tests="ON" if tests else "OFF" ) - make = "make -j " + str(NCPUS) + make = f"make -j {str(NCPUS)}" make_tests = "make tests" unit_test_binary = os.path.join(cwd, "bin", "unit_tests") @@ -79,7 +79,7 @@ def run(cmd): description="Build and test exiv2 using a matrix of build switches") parser.add_argument( "--compilers", - help="Compilers to be used to build exiv2 (when none ore specified, " + help="Compilers to be used to build exiv2 (when none are specified, " "then the default compiler will be used)", nargs='*', default=["gcc", "clang"], @@ -116,7 +116,7 @@ def run(cmd): help="Additional flags for cmake", type=str, nargs='?', - default="-DEXIV2_TEAM_EXTRA_WARNINGS=ON -DEXIV2_ENABLE_VIDEO=ON " + default="-DEXIV2_TEAM_EXTRA_WARNINGS=ON " "-DEXIV2_ENABLE_WEBREADY=ON -DEXIV2_BUILD_UNIT_TESTS=ON -DEXIV2_ENABLE_BMFF=ON " "-DBUILD_WITH_CCACHE=ON -DEXIV2_ENABLE_CURL=ON" ) diff --git a/cmake/Doxyfile.in b/cmake/Doxyfile.in index 230739fc23..d333b8df26 100644 --- a/cmake/Doxyfile.in +++ b/cmake/Doxyfile.in @@ -268,7 +268,7 @@ OPTIMIZE_OUTPUT_VHDL = NO # parses. With this tag you can assign which parser to use for a given # extension. Doxygen has a built-in mapping, but you can override or extend it # using this tag. The format is ext=language, where ext is a file extension, and -# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, # C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: # FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: # Fortran. In the later case the parser tries to guess whether the code is fixed @@ -675,7 +675,7 @@ SHOW_NAMESPACES = YES # The FILE_VERSION_FILTER tag can be used to specify a program or script that # doxygen should invoke to get the current version for each file (typically from # the version control system). Doxygen will invoke the program by executing (via -# popen()) the command command input-file, where command is the value of the +# popen()) the command input-file, where command is the value of the # FILE_VERSION_FILTER tag, and input-file is the name of an input file provided # by doxygen. Whatever the program writes to standard output is used as the file # version. For an example see the documentation. @@ -830,7 +830,6 @@ EXCLUDE = @ROOTDIR@/samples/Jzon.h \ @ROOTDIR@/src/getopt.hpp \ @ROOTDIR@/src/localtime.c \ @ROOTDIR@/src/fff.h \ - @ROOTDIR@/src/private.h \ @ROOTDIR@/src/timegm.h \ @ROOTDIR@/src/tzfile.h \ @ROOTDIR@/src/unused.h \ @@ -1474,7 +1473,7 @@ EXT_LINKS_IN_WINDOW = NO FORMULA_FONTSIZE = 10 -# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images # generated for formulas are transparent PNGs. Transparent PNGs are not # supported properly for IE 6.0, but are supported on all modern browsers. # @@ -1486,7 +1485,7 @@ FORMULA_FONTSIZE = 10 FORMULA_TRANSPARENT = YES # Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see -# http://www.mathjax.org) which uses client side Javascript for the rendering +# http://www.mathjax.org) which uses client side JavaScript for the rendering # instead of using pre-rendered bitmaps. Use this if you do not have LaTeX # installed or if you want to formulas look prettier in the HTML output. When # enabled you may also need to install MathJax separately and configure the path @@ -1556,7 +1555,7 @@ MATHJAX_CODEFILE = SEARCHENGINE = YES # When the SERVER_BASED_SEARCH tag is enabled the search engine will be -# implemented using a web server instead of a web client using Javascript. There +# implemented using a web server instead of a web client using JavaScript. There # are two flavors of web server based searching depending on the EXTERNAL_SEARCH # setting. When disabled, doxygen will generate a PHP script for searching and # an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing diff --git a/cmake/FindBrotli.cmake b/cmake/FindBrotli.cmake new file mode 100644 index 0000000000..861a66693c --- /dev/null +++ b/cmake/FindBrotli.cmake @@ -0,0 +1,45 @@ +#*************************************************************************** +# _ _ ____ _ +# Project ___| | | | _ \| | +# / __| | | | |_) | | +# | (__| |_| | _ <| |___ +# \___|\___/|_| \_\_____| +# +# Copyright (C) 1998 - 2020, Daniel Stenberg, , et al. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at https://curl.se/docs/copyright.html. +# +# You may opt to use, copy, modify, merge, publish, distribute and/or sell +# copies of the Software, and permit persons to whom the Software is +# furnished to do so, under the terms of the COPYING file. +# +# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +# KIND, either express or implied. +# +########################################################################### +include(FindPackageHandleStandardArgs) + +find_path(BROTLI_INCLUDE_DIR "brotli/decode.h") + +find_library(BROTLICOMMON_LIBRARY NAMES brotlicommon) +find_library(BROTLIDEC_LIBRARY NAMES brotlidec) + +find_package_handle_standard_args(Brotli + FOUND_VAR + BROTLI_FOUND + REQUIRED_VARS + BROTLIDEC_LIBRARY + BROTLICOMMON_LIBRARY + BROTLI_INCLUDE_DIR + FAIL_MESSAGE + "Could NOT find Brotli" +) + +set(Brotli_INCLUDE_DIRS ${BROTLI_INCLUDE_DIR}) +set(Brotli_LIBRARIES ${BROTLIDEC_LIBRARY} ${BROTLICOMMON_LIBRARY} ) + +mark_as_advanced(BROTLI_INCLUDE_DIR) +mark_as_advanced(BROTLICOMMON_LIBRARY) +mark_as_advanced(BROTLIDEC_LIBRARY) diff --git a/cmake/FindIconv.cmake b/cmake/FindIconv.cmake deleted file mode 100644 index e263c4ccec..0000000000 --- a/cmake/FindIconv.cmake +++ /dev/null @@ -1,132 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -# This code is disabled for Visual Studio as explained in README.md -if ( NOT MSVC ) - -#[=======================================================================[.rst: -FindIconv ---------- - -This module finds the ``iconv()`` POSIX.1 functions on the system. -These functions might be provided in the regular C library or externally -in the form of an additional library. - -The following variables are provided to indicate iconv support: - -.. variable:: Iconv_FOUND - - Variable indicating if the iconv support was found. - -.. variable:: Iconv_INCLUDE_DIRS - - The directories containing the iconv headers. - -.. variable:: Iconv_LIBRARIES - - The iconv libraries to be linked. - -.. variable:: Iconv_IS_BUILT_IN - - A variable indicating whether iconv support is stemming from the - C library or not. Even if the C library provides `iconv()`, the presence of - an external `libiconv` implementation might lead to this being false. - -Additionally, the following :prop_tgt:`IMPORTED` target is being provided: - -.. variable:: Iconv::Iconv - - Imported target for using iconv. - -The following cache variables may also be set: - -.. variable:: Iconv_INCLUDE_DIR - - The directory containing the iconv headers. - -.. variable:: Iconv_LIBRARY - - The iconv library (if not implicitly given in the C library). - -.. note:: - On POSIX platforms, iconv might be part of the C library and the cache - variables ``Iconv_INCLUDE_DIR`` and ``Iconv_LIBRARY`` might be empty. - -#]=======================================================================] - -if (WIN32) - # If neither C nor CXX are loaded, implicit iconv makes no sense. - set(Iconv_IS_BUILT_IN FALSE) -endif() - -# iconv can only be provided in libc on a POSIX system. -# If any cache variable is already set, we'll skip this test. -if(NOT DEFINED Iconv_IS_BUILT_IN) - if(UNIX AND NOT DEFINED Iconv_INCLUDE_DIR AND NOT DEFINED Iconv_LIBRARY) - include(CheckCSourceCompiles) - # We always suppress the message here: Otherwise on supported systems - # not having iconv in their C library (e.g. those using libiconv) - # would always display a confusing "Looking for iconv - not found" message - set(CMAKE_FIND_QUIETLY TRUE) - # The following code will not work, but it's sufficient to see if it compiles. - # Note: libiconv will define the iconv functions as macros, so CheckSymbolExists - # will not yield correct results. - set(Iconv_IMPLICIT_TEST_CODE - " - #include - #include - int main() { - char *a, *b; - size_t i, j; - iconv_t ic; - ic = iconv_open(\"to\", \"from\"); - iconv(ic, &a, &i, &b, &j); - iconv_close(ic); - } - " - ) - if(CMAKE_C_COMPILER_LOADED) - check_c_source_compiles("${Iconv_IMPLICIT_TEST_CODE}" Iconv_IS_BUILT_IN) - else() - check_cxx_source_compiles("${Iconv_IMPLICIT_TEST_CODE}" Iconv_IS_BUILT_IN) - endif() - else() - set(Iconv_IS_BUILT_IN FALSE) - endif() -endif() - -if(NOT Iconv_IS_BUILT_IN) - find_path(Iconv_INCLUDE_DIR - NAMES "iconv.h" - DOC "iconv include directory") - set(Iconv_LIBRARY_NAMES "iconv" "libiconv") -else() - set(Iconv_INCLUDE_DIR "" CACHE FILEPATH "iconv include directory") - set(Iconv_LIBRARY_NAMES "c") -endif() - -find_library(Iconv_LIBRARY - NAMES ${Iconv_LIBRARY_NAMES} - DOC "iconv library (potentially the C library)") - -mark_as_advanced(Iconv_INCLUDE_DIR) -mark_as_advanced(Iconv_LIBRARY) - -include(FindPackageHandleStandardArgs) -if(NOT Iconv_IS_BUILT_IN) - find_package_handle_standard_args(Iconv REQUIRED_VARS Iconv_LIBRARY Iconv_INCLUDE_DIR) -else() - find_package_handle_standard_args(Iconv REQUIRED_VARS Iconv_LIBRARY) -endif() - -if(Iconv_FOUND) - set(Iconv_INCLUDE_DIRS "${Iconv_INCLUDE_DIR}") - set(Iconv_LIBRARIES "${Iconv_LIBRARY}") - if(NOT TARGET Iconv::Iconv) - add_library(Iconv::Iconv INTERFACE IMPORTED) - endif() - set_property(TARGET Iconv::Iconv PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${Iconv_INCLUDE_DIRS}") - set_property(TARGET Iconv::Iconv PROPERTY INTERFACE_LINK_LIBRARIES "${Iconv_LIBRARIES}") -endif() - -endif() diff --git a/cmake/FindRegex.cmake b/cmake/FindRegex.cmake deleted file mode 100644 index be9c0c339b..0000000000 --- a/cmake/FindRegex.cmake +++ /dev/null @@ -1,50 +0,0 @@ -# - Try to find the Regex library -# -# Once done this will define -# -# REGEX_FOUND - system has libregex -# REGEX_INCLUDE_DIR - the libregex include directory -# REGEX_LIBRARIES - Link these to use libregex -# -# Copyright (c) 2018, Gilles Caulier, -# -# Redistribution and use is allowed according to the terms of the BSD license. -# For details see the accompanying COPYING-CMAKE-SCRIPTS file. - -if ( NOT MSVC AND NOT MINGW AND NOT MSYS ) - find_path(Regex_INCLUDE_DIR - NAMES regex.h - DOC "libregex include directory" - ) - - mark_as_advanced(Regex_INCLUDE_DIR) - - find_library(Regex_LIBRARY "regex" - DOC "libregex libraries" - ) - - mark_as_advanced(Regex_LIBRARY) - - find_package_handle_standard_args(Regex - FOUND_VAR Regex_FOUND - REQUIRED_VARS Regex_INCLUDE_DIR - FAIL_MESSAGE "Failed to find libregex" - ) - - if(REGEX_FOUND) - - set(REGEX_INCLUDE_DIRS ${Regex_INCLUDE_DIRS}) - - if(Regex_LIBRARY) - - set(REGEX_LIBRARIES ${Regex_LIBRARY}) - - else() - - unset(REGEX_LIBRARIES) - - endif() - - endif() -endif() - diff --git a/cmake/Findinih.cmake b/cmake/Findinih.cmake new file mode 100644 index 0000000000..ad42c1c039 --- /dev/null +++ b/cmake/Findinih.cmake @@ -0,0 +1,45 @@ +set(inih_LIBRARY_NAMES "inih" "libinih") +set(inih_inireader_LIBRARY_NAMES "INIReader" "libINIReader") + +find_path(inih_INCLUDE_DIR + NAMES "ini.h" + DOC "inih include directory") + +find_path(inih_inireader_INCLUDE_DIR + NAMES "INIReader.h" + DOC "INIReader include directory") + +find_library(inih_LIBRARY + NAMES ${inih_LIBRARY_NAMES} + DOC "inih library") + +find_library(inih_inireader_LIBRARY + NAMES ${inih_inireader_LIBRARY_NAMES} + DOC "inih library") + +mark_as_advanced(inih_INCLUDE_DIR) +mark_as_advanced(inih_LIBRARY) +mark_as_advanced(inih_inireader_INCLUDE_DIR) +mark_as_advanced(inih_inireader_LIBRARY) + +find_package_handle_standard_args(inih REQUIRED_VARS inih_LIBRARY inih_INCLUDE_DIR inih_inireader_INCLUDE_DIR inih_inireader_LIBRARY) + +if(NOT inih_FOUND) + message(FATAL_ERROR "inih library not found") +endif() + +set(inih_INCLUDE_DIRS "${inih_INCLUDE_DIR}") +set(inih_LIBRARIES "${inih_LIBRARY}") +if(NOT TARGET inih::libinih) + add_library(inih::libinih INTERFACE IMPORTED) +endif() +set_property(TARGET inih::libinih PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${inih_INCLUDE_DIRS}") +set_property(TARGET inih::libinih PROPERTY INTERFACE_LINK_LIBRARIES "${inih_LIBRARIES}") + +set(inih_inireader_INCLUDE_DIRS "${inih_inireader_INCLUDE_DIR}") +set(inih_inireader_LIBRARIES "${inih_inireader_LIBRARY}") +if(NOT TARGET inih::inireader) + add_library(inih::inireader INTERFACE IMPORTED) +endif() +set_property(TARGET inih::inireader PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${inih_inireader_INCLUDE_DIRS}") +set_property(TARGET inih::inireader PROPERTY INTERFACE_LINK_LIBRARIES "${inih_inireader_LIBRARIES}") diff --git a/cmake/JoinPaths.cmake b/cmake/JoinPaths.cmake index cfd54c9c98..36bcb13ede 100644 --- a/cmake/JoinPaths.cmake +++ b/cmake/JoinPaths.cmake @@ -1,5 +1,5 @@ # This module provides function for joining paths -# known from from most languages +# known from most languages # # Original license: # SPDX-License-Identifier: (MIT OR CC0-1.0) diff --git a/cmake/compilerFlags.cmake b/cmake/compilerFlags.cmake index 35faf5010f..a1aaedafa3 100644 --- a/cmake/compilerFlags.cmake +++ b/cmake/compilerFlags.cmake @@ -1,6 +1,14 @@ # These flags applies to exiv2lib, the applications, and to the xmp code include(CheckCXXCompilerFlag) +if (NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 20) +endif() +set(CMAKE_CXX_STANDARD_REQUIRED ON) +if (CYGWIN) # Cygwin and MSYS + set(CMAKE_CXX_EXTENSIONS ON) +endif() + if ( MINGW OR UNIX OR MSYS ) # MINGW, Linux, APPLE, CYGWIN if (${CMAKE_CXX_COMPILER_ID} STREQUAL GNU) set(COMPILER_IS_GCC ON) @@ -25,7 +33,9 @@ if ( MINGW OR UNIX OR MSYS ) # MINGW, Linux, APPLE, CYGWIN if (COMPILER_IS_GCC OR COMPILER_IS_CLANG) # This fails under Fedora - MinGW - Gcc 8.3 if (NOT (MINGW OR CYGWIN OR CMAKE_HOST_SOLARIS)) - check_cxx_compiler_flag(-fstack-clash-protection HAS_FSTACK_CLASH_PROTECTION) + if (NOT APPLE) # Don't know why this isn't working correctly on Apple with M1 processor + check_cxx_compiler_flag(-fstack-clash-protection HAS_FSTACK_CLASH_PROTECTION) + endif() check_cxx_compiler_flag(-fcf-protection HAS_FCF_PROTECTION) check_cxx_compiler_flag(-fstack-protector-strong HAS_FSTACK_PROTECTOR_STRONG) if(HAS_FSTACK_CLASH_PROTECTION) @@ -34,15 +44,16 @@ if ( MINGW OR UNIX OR MSYS ) # MINGW, Linux, APPLE, CYGWIN if(HAS_FCF_PROTECTION) add_compile_options(-fcf-protection) endif() - if(HAS_FSTACK_PROTECTOR_STRONG) + if(BUILD_WITH_STACK_PROTECTOR AND HAS_FSTACK_PROTECTOR_STRONG) add_compile_options(-fstack-protector-strong) + add_link_options(-fstack-protector-strong) endif() endif() - add_compile_options(-Wp,-D_GLIBCXX_ASSERTIONS) + add_compile_options(-D_GLIBCXX_ASSERTIONS) if (CMAKE_BUILD_TYPE STREQUAL Release AND NOT (APPLE OR MINGW OR MSYS)) - add_compile_options(-Wp,-D_FORTIFY_SOURCE=2) # Requires to compile with -O2 + add_compile_options(-D_FORTIFY_SOURCE=2) # Requires to compile with -O2 endif() if(BUILD_WITH_COVERAGE) @@ -53,10 +64,26 @@ if ( MINGW OR UNIX OR MSYS ) # MINGW, Linux, APPLE, CYGWIN endif() add_compile_options(-Wall -Wcast-align -Wpointer-arith -Wformat-security -Wmissing-format-attribute -Woverloaded-virtual -W) + add_compile_options(-Wno-error=format-nonliteral) # This seems to be causing issues in the Fedora_MinGW GitLab job #add_compile_options(-fasynchronous-unwind-tables) + # The EXIV2_TEAM_OSS_FUZZ option is used by the OSS-Fuzz build script: + # https://github.com/google/oss-fuzz/tree/master/projects/exiv2/build.sh + # OSS-Fuzz wants full control of the sanitizer flags, so we don't add + # the `-fsanitize=fuzzer-no-link` flag when building for OSS-Fuzz. + if( EXIV2_BUILD_FUZZ_TESTS AND NOT EXIV2_TEAM_OSS_FUZZ ) + if (NOT COMPILER_IS_CLANG) + message(FATAL_ERROR "You need to build with Clang for the fuzzers to work. " + "Use Clang") + endif() + set(FUZZER_FLAGS "-fsanitize=fuzzer-no-link") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${FUZZER_FLAGS}") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${FUZZER_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${FUZZER_FLAGS}") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${FUZZER_FLAGS}") + endif() if ( EXIV2_TEAM_USE_SANITIZERS ) # ASAN is available in gcc from 4.8 and UBSAN from 4.9 @@ -98,6 +125,7 @@ if(MSVC) PATHS ENV CLCACHE_PATH PATH_SUFFIXES Scripts clcache-4.1.0 ) + if (CLCACHE) message(STATUS "clcache found in ${CLCACHE}") if (CMAKE_BUILD_TYPE STREQUAL "Debug") @@ -107,6 +135,12 @@ if(MSVC) endif() endif() + # Make Debug builds a little faster without sacrificing debugging experience + #set (CMAKE_CXX_FLAGS_DEBUG "/MDd /Zi /Ob0 /Od /RTC1") + set (CMAKE_CXX_FLAGS_DEBUG "/MDd /Zi /Ob0 /Ox /Zo") + # /Ox (Enable Most Speed Optimizations) + # /Zo (Enhance Optimized Debugging) + set(variables CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_MINSIZEREL @@ -137,13 +171,7 @@ if(MSVC) string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") endif () - # Object Level Parallelism - add_compile_options(/MP) - add_definitions(-DNOMINMAX) # This definition is not only needed for Exiv2 but also for xmpsdk - - # https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/ - if (MSVC_VERSION GREATER_EQUAL "1910") # VS2017 and up - add_compile_options("/Zc:__cplusplus") - endif() - + add_compile_options(/MP) # Object Level Parallelism + add_compile_options(/utf-8) # Set source and execution character sets to UTF-8 + add_definitions(-DNOMINMAX) # This definition is not only needed for Exiv2 but also for xmpsdk endif() diff --git a/cmake/compilerFlagsExiv2.cmake b/cmake/compilerFlagsExiv2.cmake index d364b69558..07b1bdeda3 100644 --- a/cmake/compilerFlagsExiv2.cmake +++ b/cmake/compilerFlagsExiv2.cmake @@ -4,10 +4,10 @@ include(CheckCXXCompilerFlag) if (COMPILER_IS_GCC OR COMPILER_IS_CLANG) # MINGW, Linux, APPLE, CYGWIN if ( EXIV2_TEAM_WARNINGS_AS_ERRORS ) - add_compile_options(-Werror -Wno-error=deprecated-declarations) - check_cxx_compiler_flag(-Wno-error=deprecated-copy DEPRECATED_COPY) + add_compile_options(-Werror) + check_cxx_compiler_flag(-Wdeprecated-copy DEPRECATED_COPY) if ( DEPRECATED_COPY) - add_compile_options(-Wno-error=deprecated-copy) + add_compile_options(-Wdeprecated-copy) endif () endif () @@ -21,10 +21,10 @@ if (COMPILER_IS_GCC OR COMPILER_IS_CLANG) # MINGW, Linux, APPLE, CYGWIN " -Wlogical-op" " -Wdouble-promotion" " -Wshadow" - " -Wuseless-cast" " -Wpointer-arith" # This warning is also enabled by -Wpedantic " -Wformat=2" #" -Wold-style-cast" + #" -Wuseless-cast" Disabled mainly because of conversion of socket types (different types on OSs) ) endif () diff --git a/cmake/config.h.cmake b/cmake/config.h.cmake index b633d12778..c171129408 100644 --- a/cmake/config.h.cmake +++ b/cmake/config.h.cmake @@ -1,28 +1,31 @@ // File generated by cmake from cmake/config.h.cmake. -#ifndef _EXV_CONF_H_ -#define _EXV_CONF_H_ - -// Defined if you want to use libssh for SshIO. -#cmakedefine EXV_USE_SSH +#ifndef EXV_CONF_H_ +#define EXV_CONF_H_ // Define to 1 if you want to use libcurl in httpIO. #cmakedefine EXV_USE_CURL +// Define to 1 if you want to enable filesystem access +#cmakedefine EXV_ENABLE_FILESYSTEM + // Define if you require webready support. #cmakedefine EXV_ENABLE_WEBREADY -// Define if you have the header file. -#cmakedefine EXV_HAVE_LIBINTL_H - // Define if you want translation of program messages to the user's native language #cmakedefine EXV_ENABLE_NLS +// Define if you want to enable the decoding of video metadata +#cmakedefine EXV_ENABLE_VIDEO + // Define if you want BMFF support. #cmakedefine EXV_ENABLE_BMFF -// Define if you want video support. -#cmakedefine EXV_ENABLE_VIDEO +// Define if you want to use the inih library. +#cmakedefine EXV_ENABLE_INIH + +// Define if you have the std::format function. +#cmakedefine EXV_HAVE_STD_FORMAT // Define if you have the strerror_r function. #cmakedefine EXV_HAVE_STRERROR_R @@ -30,52 +33,26 @@ // Define if the strerror_r function returns char*. #cmakedefine EXV_STRERROR_R_CHAR_P -// Define to enable the Windows unicode path support. -#cmakedefine EXV_UNICODE_PATH +#if defined(__NetBSD__) +#include +#if __NetBSD_Prereq__(9,99,17) +#define NETBSD_POSIX_ICONV 1 +#else +#define NETBSD_POSIX_ICONV 0 +#endif +#endif -/* Define to `const' or to empty, depending on the second argument of `iconv'. */ -#cmakedefine ICONV_ACCEPTS_CONST_INPUT -#if defined(ICONV_ACCEPTS_CONST_INPUT) || defined(__NetBSD__) +#if (defined(__NetBSD__) && !NETBSD_POSIX_ICONV) #define EXV_ICONV_CONST const #else #define EXV_ICONV_CONST #endif -// Define if you have the header file. -#cmakedefine EXV_HAVE_REGEX_H - -// Define if have the header file. -#cmakedefine EXV_HAVE_MEMORY_H - -// Define if stdbool.h conforms to C99. -#cmakedefine EXV_HAVE_STDBOOL_H - -// Define if you have the header file. -#cmakedefine EXV_HAVE_STRINGS_H - -// Define if you have the mmap function. -#cmakedefine EXV_HAVE_MMAP - -// Define if you have the munmap function. -#cmakedefine EXV_HAVE_MUNMAP - -// Define if you have header file. -#cmakedefine EXV_HAVE_SYS_STAT_H - -// Define if you have the header file. -#cmakedefine EXV_HAVE_SYS_TYPES_H - -/* Define if you have the header file. */ -#cmakedefine EXV_HAVE_UNISTD_H - -// Define if you have the header file. -#cmakedefine EXV_HAVE_SYS_MMAN_H - -// Define if you have are using the zlib library. +// Define if you have the zlib library. #cmakedefine EXV_HAVE_LIBZ -// Define if you have the header file. -#cmakedefine EXV_HAVE_PROCESS_H +// Define if you have the brotli library. +#cmakedefine EXV_HAVE_BROTLI /* Define if you have (Exiv2/xmpsdk) Adobe XMP Toolkit. */ #cmakedefine EXV_HAVE_XMP_TOOLKIT @@ -89,10 +66,10 @@ /* Define to the version of this package. */ #cmakedefine EXV_PACKAGE_VERSION "@PROJECT_VERSION@" -#define EXIV2_MAJOR_VERSION (@PROJECT_VERSION_MAJOR@) -#define EXIV2_MINOR_VERSION (@PROJECT_VERSION_MINOR@) -#define EXIV2_PATCH_VERSION (@PROJECT_VERSION_PATCH@) -#define EXIV2_TWEAK_VERSION (@PROJECT_VERSION_TWEAK@) +#define EXIV2_MAJOR_VERSION (@PROJECT_VERSION_MAJOR@U) +#define EXIV2_MINOR_VERSION (@PROJECT_VERSION_MINOR@U) +#define EXIV2_PATCH_VERSION (@PROJECT_VERSION_PATCH@U) +#define EXIV2_TWEAK_VERSION (@PROJECT_VERSION_TWEAK@U) // Definition to enable translation of Nikon lens names. #cmakedefine EXV_HAVE_LENSDATA @@ -100,7 +77,4 @@ // Define if you have the iconv function. #cmakedefine EXV_HAVE_ICONV -// Definition to enable conversion of UCS2 encoded Windows tags to UTF-8. -#cmakedefine EXV_HAVE_PRINTUCS2 - -#endif /* !_EXV_CONF_H_ */ +#endif /* !EXV_CONF_H_ */ diff --git a/cmake/exiv2-config.cmake.in b/cmake/exiv2-config.cmake.in new file mode 100644 index 0000000000..0270abf343 --- /dev/null +++ b/cmake/exiv2-config.cmake.in @@ -0,0 +1,64 @@ +@PACKAGE_INIT@ + +cmake_minimum_required(VERSION 3.12) +include(CMakeFindDependencyMacro) + +if(NOT @BUILD_SHARED_LIBS@) # if(NOT BUILD_SHARED_LIBS) + if(@EXIV2_ENABLE_PNG@) # if(EXIV2_ENABLE_PNG) + find_dependency(ZLIB REQUIRED) + endif() + + if(@EXIV2_ENABLE_BMFF@ AND @EXIV2_ENABLE_BROTLI@) # if(EXIV2_ENABLE_BMFF AND EXIV2_ENABLE_BROTLI) + find_package(Brotli QUIET) + if (NOT Brotli_FOUND) + message(FATAL_ERROR + "Static builds of exiv2 require Brotli. " + "Please provide FindBrotli.cmake on CMAKE_MODULE_PATH " + "or point to the cmake/ dir in the exiv2 source tree.") + endif() + endif() + + if(@EXIV2_ENABLE_WEBREADY@) # if(EXIV2_ENABLE_WEBREADY) + if(@EXIV2_ENABLE_CURL@) # if(EXIV2_ENABLE_CURL) + find_dependency(CURL REQUIRED) + endif() + endif() + + if(@EXIV2_ENABLE_XMP@) # if(EXIV2_ENABLE_XMP) + find_dependency(EXPAT REQUIRED) + elseif(@EXIV2_ENABLE_EXTERNAL_XMP@) # elseif(EXIV2_ENABLE_EXTERNAL_XMP) + find_dependency(XmpSdk REQUIRED) + endif() + + if(@EXIV2_ENABLE_NLS@) # if(EXIV2_ENABLE_NLS) + find_dependency(Intl REQUIRED) + endif() + + if(@EXV_HAVE_LIBICONV@) # if(EXV_HAVE_LIBICONV) + find_dependency(Iconv REQUIRED) + endif() + + if(@EXIV2_ENABLE_INIH@) # if(EXIV2_ENABLE_INIH) + find_package(inih QUIET) + if (NOT inih_FOUND) + message(FATAL_ERROR + "Static builds of exiv2 require inih. " + "Please provide Findinih.cmake on CMAKE_MODULE_PATH " + "or point to the cmake/ dir in the exiv2 source tree.") + endif() + endif() + + if(NOT "@EXV_HAVE_STD_FORMAT@") # if(NOT EXV_HAVE_STD_FORMAT) + find_dependency(fmt "5.0.0" REQUIRED) + endif() +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/exiv2-targets.cmake") + +check_required_components(exiv2) + +# compatibility with non-aliased users +if(NOT TARGET exiv2lib) + add_library(exiv2lib ALIAS Exiv2::exiv2lib) +endif() + diff --git a/cmake/exiv2.pc.in b/cmake/exiv2.pc.in index cea7604229..fb8f800331 100644 --- a/cmake/exiv2.pc.in +++ b/cmake/exiv2.pc.in @@ -1,11 +1,13 @@ prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} -libdir=@libdir_for_pc_file@ -includedir=@includedir_for_pc_file@ +libdir=@EXIV2_LIBDIR@ +includedir=@EXIV2_INCLUDEDIR@ Name: exiv2 Description: @PROJECT_DESCRIPTION@ Version: @PROJECT_VERSION@ URL: @PACKAGE_URL@ +Requires.private: @requires_private_for_pc_file@ Libs: -L${libdir} -lexiv2 +Libs.private: @libs_private_for_pc_file@ Cflags: -I${includedir} diff --git a/cmake/findDependencies.cmake b/cmake/findDependencies.cmake index ec3a43f5c5..5562edb9fd 100644 --- a/cmake/findDependencies.cmake +++ b/cmake/findDependencies.cmake @@ -1,41 +1,59 @@ -# set include path for FindXXX.cmake files -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") +if (CONAN_AUTO_INSTALL) + # Download automatically the cmake-conan integration file + if(NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake") + message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan") + file(DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake-conan/develop/conan.cmake" + "${CMAKE_BINARY_DIR}/conan.cmake" + TLS_VERIFY ON) + endif() + + include(${CMAKE_BINARY_DIR}/conan.cmake) + + conan_cmake_autodetect(settings) + conan_cmake_install(PATH_OR_REFERENCE .. + BUILD missing + REMOTE conancenter + OPTIONS webready=True + SETTINGS ${settings}) +endif() -# don't use Frameworks on the Mac (#966) if (APPLE) - set(CMAKE_FIND_FRAMEWORK NEVER) + # On Apple, we use the conan cmake_paths generator + if (EXISTS ${CMAKE_BINARY_DIR}/conan_paths.cmake) + include(${CMAKE_BINARY_DIR}/conan_paths.cmake) + endif() +else() + # Otherwise, we rely on the conan cmake_find_package generator + list(APPEND CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR}) + list(APPEND CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR}) endif() -# Check if the conan file exist to find the dependencies -if (EXISTS ${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) - set(USING_CONAN ON) - include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) - conan_basic_setup(NO_OUTPUT_DIRS KEEP_RPATHS TARGETS) +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/") + +if(BUILD_TESTING) + find_package(Python3 COMPONENTS Interpreter) + if(NOT Python3_Interpreter_FOUND) + message(WARNING "Python3 was not found. Python tests under the 'tests' folder will not be executed.") + endif() endif() -find_package(Threads REQUIRED) +# don't use Frameworks on the Mac (#966) +if (APPLE) + set(CMAKE_FIND_FRAMEWORK NEVER) +endif() if( EXIV2_ENABLE_PNG ) find_package( ZLIB REQUIRED ) endif( ) +if( EXIV2_ENABLE_BMFF AND EXIV2_ENABLE_BROTLI ) + find_package( Brotli REQUIRED ) +endif( ) + if( EXIV2_ENABLE_WEBREADY ) if( EXIV2_ENABLE_CURL ) find_package(CURL REQUIRED) endif() - - if( EXIV2_ENABLE_SSH ) - find_package(libssh CONFIG REQUIRED) - # Define an imported target to have compatibility with <=libssh-0.9.0 - # libssh-0.9.1 is broken regardless. - if(NOT TARGET ssh) - add_library(ssh SHARED IMPORTED) - set_target_properties(ssh PROPERTIES - IMPORTED_LOCATION "${LIBSSH_LIBRARIES}" - INTERFACE_INCLUDE_DIRECTORIES "${LIBSSH_INCLUDE_DIR}" - ) - endif() - endif() endif() if (EXIV2_ENABLE_XMP AND EXIV2_ENABLE_EXTERNAL_XMP) @@ -54,8 +72,16 @@ endif( ) find_package(Iconv) if( ICONV_FOUND ) - message ( "-- ICONV_INCLUDE_DIR : " ${Iconv_INCLUDE_DIR} ) - message ( "-- ICONV_LIBRARIES : " ${Iconv_LIBRARY} ) + message ( "-- Iconv_INCLUDE_DIRS : " ${Iconv_INCLUDE_DIRS} ) + message ( "-- Iconv_LIBRARIES : " ${Iconv_LIBRARIES} ) +endif() + +if( EXIV2_ENABLE_INIH ) + find_package(inih) + message ( "-- inih_INCLUDE_DIRS : " ${inih_INCLUDE_DIRS} ) + message ( "-- inih_LIBRARIES : " ${inih_LIBRARIES} ) + message ( "-- inih_inireader_INCLUDE_DIRS : " ${inih_inireader_INCLUDE_DIRS} ) + message ( "-- inih_inireader_LIBRARIES : " ${inih_inireader_LIBRARIES} ) endif() if( BUILD_WITH_CCACHE ) @@ -66,4 +92,3 @@ if( BUILD_WITH_CCACHE ) set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache) endif() endif() - diff --git a/cmake/gcovr.cmake b/cmake/gcovr.cmake new file mode 100644 index 0000000000..0a3472258a --- /dev/null +++ b/cmake/gcovr.cmake @@ -0,0 +1,24 @@ +# Intended usage +# cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_WITH_COVERAGE=yes ../ +# cmake --build . --config Debug +# ctest +# cmake --build . --config Debug --target coverage + +if(BUILD_WITH_COVERAGE) + find_program (GCOVR gcovr) + + if(GCOVR) + file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/coverage_output ) + add_custom_command(OUTPUT _run_gcovr_parser + POST_BUILD + COMMAND ${GCOVR} --root ${PROJECT_SOURCE_DIR} --object-dir=${CMAKE_BINARY_DIR} --html --html-details -o coverage_output/coverage.html + --exclude-directories xmpsdk --exclude-directories unitTests --exclude-directories samples + --exclude '.*xmpsdk.*' --exclude '.*unitTests.*' --exclude '.*samples.*' + --exclude-unreachable-branches --exclude-throw-branches + + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + add_custom_target (coverage DEPENDS _run_gcovr_parser) + endif() + +endif(BUILD_WITH_COVERAGE) diff --git a/cmake/generateConfigFile.cmake b/cmake/generateConfigFile.cmake index 3f79753166..209b223d41 100644 --- a/cmake/generateConfigFile.cmake +++ b/cmake/generateConfigFile.cmake @@ -4,14 +4,13 @@ include(CheckCXXSymbolExists) # Note that the scope of the EXV_ variables in local if (${EXIV2_ENABLE_WEBREADY}) - set(EXV_USE_SSH ${EXIV2_ENABLE_SSH}) set(EXV_USE_CURL ${EXIV2_ENABLE_CURL}) endif() -set(EXV_ENABLE_BMFF ${EXIV2_ENABLE_BMFF}) -set(EXV_ENABLE_VIDEO ${EXIV2_ENABLE_VIDEO}) -set(EXV_ENABLE_WEBREADY ${EXIV2_ENABLE_WEBREADY}) -set(EXV_HAVE_LENSDATA ${EXIV2_ENABLE_LENSDATA}) -set(EXV_HAVE_PRINTUCS2 ${EXIV2_ENABLE_PRINTUCS2}) +set(EXV_ENABLE_BMFF ${EXIV2_ENABLE_BMFF}) +set(EXV_ENABLE_WEBREADY ${EXIV2_ENABLE_WEBREADY}) +set(EXV_HAVE_LENSDATA ${EXIV2_ENABLE_LENSDATA}) +set(EXV_ENABLE_INIH ${EXIV2_ENABLE_INIH}) +set(EXV_ENABLE_FILESYSTEM ${EXIV2_ENABLE_FILESYSTEM_ACCESS}) set(EXV_PACKAGE_NAME ${PROJECT_NAME}) set(EXV_PACKAGE_VERSION ${PROJECT_VERSION}) @@ -23,11 +22,9 @@ else() endif() set(EXV_HAVE_ICONV ${ICONV_FOUND}) set(EXV_HAVE_LIBZ ${ZLIB_FOUND}) -set(EXV_UNICODE_PATH ${EXIV2_ENABLE_WIN_UNICODE}) +set(EXV_HAVE_BROTLI ${BROTLI_FOUND}) -check_cxx_symbol_exists(gmtime_r time.h EXV_HAVE_GMTIME_R) -check_cxx_symbol_exists(mmap sys/mman.h EXV_HAVE_MMAP ) -check_cxx_symbol_exists(munmap sys/mman.h EXV_HAVE_MUNMAP ) +check_cxx_source_compiles("#include \nint main(){std::format(\"t\");}" EXV_HAVE_STD_FORMAT) check_cxx_symbol_exists(strerror_r string.h EXV_HAVE_STRERROR_R ) check_cxx_source_compiles( " @@ -35,22 +32,11 @@ check_cxx_source_compiles( " int main() { char buff[100]; const char* c = strerror_r(0,buff,100); + (void)c; // ignore unuse-variable return 0; }" EXV_STRERROR_R_CHAR_P ) -check_include_file_cxx( "memory.h" EXV_HAVE_MEMORY_H ) -check_include_file_cxx( "process.h" EXV_HAVE_PROCESS_H ) -check_include_file_cxx( "stdbool.h" EXV_HAVE_STDBOOL_H ) -check_include_file_cxx( "strings.h" EXV_HAVE_STRINGS_H ) -check_include_file_cxx( "sys/stat.h" EXV_HAVE_SYS_STAT_H ) -check_include_file_cxx( "sys/types.h" EXV_HAVE_SYS_TYPES_H ) -check_include_file_cxx( "inttypes.h" EXV_HAVE_INTTYPES_H ) -check_include_file_cxx( "unistd.h" EXV_HAVE_UNISTD_H ) -check_include_file_cxx( "sys/mman.h" EXV_HAVE_SYS_MMAN_H ) -if ( NOT MINGW AND NOT MSYS AND NOT MSVC ) -check_include_file_cxx( "regex.h" EXV_HAVE_REGEX_H ) -endif() - set(EXV_ENABLE_NLS ${EXIV2_ENABLE_NLS}) +set(EXV_ENABLE_VIDEO ${EXIV2_ENABLE_VIDEO}) configure_file(cmake/config.h.cmake ${CMAKE_BINARY_DIR}/exv_conf.h @ONLY) diff --git a/cmake/mainSetup.cmake b/cmake/mainSetup.cmake index fcaa21fa4c..6f8e60faee 100644 --- a/cmake/mainSetup.cmake +++ b/cmake/mainSetup.cmake @@ -3,9 +3,13 @@ include(GNUInstallDirs) include(CheckFunctionExists) +include(CheckCXXSymbolExists) include(GenerateExportHeader) include(CMakeDependentOption) include(cmake/JoinPaths.cmake) +include(CTest) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) @@ -28,6 +32,13 @@ if (UNIX) endif() endif() +if(MINGW) + check_cxx_symbol_exists(_UCRT "ctime" USES_UCRT) + if(NOT USES_UCRT) + message(FATAL_ERROR "Non UCRT MinGW is unsupported. Please update toolchain") + endif() +endif() + # Prevent conflicts when exiv2 is consumed in multiple-subdirectory projects. if (NOT TARGET uninstall) configure_file(cmake/exiv2_uninstall.cmake ${CMAKE_BINARY_DIR}/cmake_uninstall.cmake COPYONLY) diff --git a/cmake/msvc_conan_profiles/msvc2019Release64 b/cmake/msvc_conan_profiles/msvc2019Release64 index 299453366f..f0d4e20ead 100755 --- a/cmake/msvc_conan_profiles/msvc2019Release64 +++ b/cmake/msvc_conan_profiles/msvc2019Release64 @@ -1,9 +1,9 @@ [build_requires] [settings] arch=x86_64 -build_type=Debug +build_type=Release compiler=Visual Studio -compiler.runtime=MDd +compiler.runtime=MD compiler.version=16 os=Windows arch_build=x86_64 diff --git a/cmake/packaging.cmake b/cmake/packaging.cmake index fdc862e89a..ac5cc0e02e 100644 --- a/cmake/packaging.cmake +++ b/cmake/packaging.cmake @@ -1,5 +1,5 @@ set(CPACK_PACKAGE_NAME "${PROJECT_NAME}") -set(CPACK_PACKAGE_CONTACT "Luis Díaz MÃƒÂĄs ") +set(CPACK_PACKAGE_CONTACT "Luis Díaz MÃĄs ") set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) set(CPACK_SOURCE_GENERATOR TGZ) @@ -12,27 +12,26 @@ else() set(CPACK_GENERATOR TGZ) # MinGW/Cygwin/Linux/macOS etc use .tar.gz endif() -set (BS "") # Bit Size -if ( NOT APPLE ) - if ( CMAKE_SIZEOF_VOID_P EQUAL 8 ) - set (BS 64) - else() - set (BS 32) - endif() +set (BARCH ${CMAKE_SYSTEM_PROCESSOR}) # Target architecture +if ( CMAKE_SIZEOF_VOID_P EQUAL 4 ) + # 32-bit build, force architecture + set (BARCH "i686") endif() set (LT "") # Library Type if ( NOT BUILD_SHARED_LIBS ) - set (LT Static) + set (LT -Static) endif() set (BT "") # Build Type if ( NOT ${CMAKE_BUILD_TYPE} STREQUAL Release ) - set (BT ${CMAKE_BUILD_TYPE}) + set (BT -${CMAKE_BUILD_TYPE}) endif() -if ( MINGW OR MSYS ) +if ( MINGW ) set (PACKDIR MinGW) +elseif ( MSYS ) + set (PACKDIR MSYS) elseif ( MSVC ) set (PACKDIR msvc) elseif ( CYGWIN ) @@ -55,24 +54,21 @@ endif() set (CC "") # Compiler if ( NOT APPLE AND NOT CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" ) if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") - set (CC Clang) + set (CC -Clang) endif() endif() -set (VI "") # Video -if ( EXIV2_ENABLE_VIDEO ) - set (VI Video) -endif() - set (WR "") # WebReady if ( EXIV2_ENABLE_WEBREADY ) - set (WR Webready) + set (WR -Webready) endif() set (VS "") # VisualStudio if ( MSVC ) - # VS2015 >= 1900, VS2017 >= 1910, VS2019 >= 1920 - if ( MSVC_VERSION GREATER 1919 ) + # VS2015 >= 1900, VS2017 >= 1910, VS2019 >= 1920, VS2022 >= 1930 + if ( MSVC_VERSION GREATER 1929 ) + set(VS 2022) + elseif ( MSVC_VERSION GREATER 1919 ) set(VS 2019) elseif ( MSVC_VERSION GREATER 1909 ) set(VS 2017) @@ -118,7 +114,7 @@ endif() # Set RV = Release Version set(RV "Exiv2 v${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") -set(CPACK_PACKAGE_FILE_NAME ${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${VS}${BUNDLE_NAME}${BS}${CC}${LT}${BT}${VI}${WR}) +set(CPACK_PACKAGE_FILE_NAME ${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${VS}${BUNDLE_NAME}-${BARCH}${CC}${LT}${BT}${WR}) # https://stackoverflow.com/questions/17495906/copying-files-and-including-them-in-a-cpack-archive install(FILES "${PROJECT_SOURCE_DIR}/samples/exifprint.cpp" DESTINATION "samples") @@ -136,7 +132,7 @@ foreach(doc ${DOCS}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${doc} DESTINATION .) endforeach() -# copy build/log which which is present if built by build.sh +# copy build/log which is present if built by build.sh if(EXISTS ${PROJECT_SOURCE_DIR}/build/logs/build.txt) install(FILES ${PROJECT_SOURCE_DIR}/build/logs/build.txt DESTINATION "logs") endif() diff --git a/cmake/printSummary.cmake b/cmake/printSummary.cmake index 2f3e1fc496..07e3c82237 100644 --- a/cmake/printSummary.cmake +++ b/cmake/printSummary.cmake @@ -38,42 +38,36 @@ message( STATUS "RelWithDebInfo: ${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO}" ) message( STATUS "MinSizeRel: ${CMAKE_EXE_LINKER_FLAGS_MINSIZEREL}" ) message( STATUS "" ) message( STATUS "Compiler Options") -OptionOutput( "Warnings as errors: " EXIV2_WARNINGS_AS_ERRORS ) -OptionOutput( "Use extra compiler warning flags: " EXIV2_EXTRA_WARNINGS ) +OptionOutput( "Warnings as errors: " EXIV2_TEAM_WARNINGS_AS_ERRORS ) +OptionOutput( "Use extra compiler warning flags: " EXIV2_TEAM_EXTRA_WARNINGS ) message( STATUS "" ) -message( STATUS "------------------------------------------------------------------" ) -OptionOutput( "Building shared library: " BUILD_SHARED_LIBS ) -OptionOutput( "Building PNG support: " EXIV2_ENABLE_PNG AND ZLIB_FOUND ) +message( STATUS "------------------------------------------------------------------" ) +OptionOutput( "Building shared library: " BUILD_SHARED_LIBS ) +OptionOutput( "Building PNG support: " EXIV2_ENABLE_PNG AND ZLIB_FOUND ) if ( EXIV2_ENABLE_EXTERNAL_XMP ) - OptionOutput( "XMP metadata support (EXTERNAL): " EXIV2_ENABLE_EXTERNAL_XMP ) + OptionOutput( "XMP metadata support (EXTERNAL): " EXIV2_ENABLE_EXTERNAL_XMP ) else() - OptionOutput( "XMP metadata support: " EXIV2_ENABLE_XMP ) + OptionOutput( "XMP metadata support: " EXIV2_ENABLE_XMP ) endif() -OptionOutput( "Building BMFF support: " EXIV2_ENABLE_BMFF ) -OptionOutput( "Native language support: " EXIV2_ENABLE_NLS ) -OptionOutput( "Conversion of Windows XP tags: " EXIV2_ENABLE_PRINTUCS2 ) -OptionOutput( "Nikon lens database: " EXIV2_ENABLE_LENSDATA ) -OptionOutput( "Building video support: " EXIV2_ENABLE_VIDEO ) -OptionOutput( "Building webready support: " EXIV2_ENABLE_WEBREADY ) +OptionOutput( "Building BMFF support: " EXIV2_ENABLE_BMFF ) +OptionOutput( "Brotli support for JPEG XL: " EXIV2_ENABLE_BMFF AND BROTLI_FOUND ) +OptionOutput( "Native language support: " EXIV2_ENABLE_NLS ) +OptionOutput( "Building video support: " EXIV2_ENABLE_VIDEO ) +OptionOutput( "Nikon lens database: " EXIV2_ENABLE_LENSDATA ) +OptionOutput( "Building webready support: " EXIV2_ENABLE_WEBREADY ) if ( EXIV2_ENABLE_WEBREADY ) - OptionOutput( "USE Libcurl for HttpIo: " EXIV2_ENABLE_CURL ) - OptionOutput( "USE Libssh for SshIo: " EXIV2_ENABLE_SSH ) + OptionOutput( "USE Libcurl for HttpIo: " EXIV2_ENABLE_CURL ) endif ( EXIV2_ENABLE_WEBREADY ) if (WIN32) - OptionOutput( "Dynamic runtime override: " EXIV2_ENABLE_DYNAMIC_RUNTIME) - OptionOutput( "Unicode paths (wstring): " EXIV2_ENABLE_WIN_UNICODE ) + OptionOutput( "Dynamic runtime override: " EXIV2_ENABLE_DYNAMIC_RUNTIME ) endif() -OptionOutput( "Building exiv2 command: " EXIV2_BUILD_EXIV2_COMMAND ) -OptionOutput( "Building samples: " EXIV2_BUILD_SAMPLES ) -OptionOutput( "Building unit tests: " EXIV2_BUILD_UNIT_TESTS ) -OptionOutput( "Building doc: " EXIV2_BUILD_DOC ) -OptionOutput( "Building with coverage flags: " BUILD_WITH_COVERAGE ) -OptionOutput( "Using ccache: " BUILD_WITH_CCACHE ) - -message( STATUS "------------------------------------------------------------------" ) - -message(STATUS " WARNING: Deprecated features: EPS, Video, Ssh") - -message( STATUS "------------------------------------------------------------------" ) +OptionOutput( "Building exiv2 command: " EXIV2_BUILD_EXIV2_COMMAND ) +OptionOutput( "Building samples: " EXIV2_BUILD_SAMPLES AND EXIV2_BUILD_EXIV2_COMMAND ) +OptionOutput( "Building unit tests: " EXIV2_BUILD_UNIT_TESTS AND BUILD_TESTING ) +OptionOutput( "Building fuzz tests: " EXIV2_BUILD_FUZZ_TESTS ) +OptionOutput( "Building doc: " EXIV2_BUILD_DOC ) +OptionOutput( "Building with coverage flags: " BUILD_WITH_COVERAGE ) +OptionOutput( "Building with filesystem access " EXIV2_ENABLE_FILESYSTEM_ACCESS ) +OptionOutput( "Using ccache: " BUILD_WITH_CCACHE ) diff --git a/cmake/toolchain/Ubuntu20_04_mingw-w64-x86_64.cmake b/cmake/toolchain/Ubuntu20_04_mingw-w64-x86_64.cmake new file mode 100644 index 0000000000..3245dc1638 --- /dev/null +++ b/cmake/toolchain/Ubuntu20_04_mingw-w64-x86_64.cmake @@ -0,0 +1,25 @@ +# Sample toolchain file for building for Windows from an Ubuntu 20.04 Linux system. +# +# Typical usage: +# *) install cross compiler: `sudo apt-get install mingw-w64` +# *) install zlib for mingw: sudo apt install libz-mingw-w64-dev +# *) mkdir buildMinGWRelease && cd buildMinGWRelease +# *) cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain/Ubuntu20_04_mingw-w64-x86_64.cmake +# -DEXIV2_ENABLE_XMP=OFF -DEXIV2_TEAM_EXTRA_WARNINGS=ON + +set(CMAKE_SYSTEM_NAME Windows) +set(TOOLCHAIN_PREFIX x86_64-w64-mingw32) + +# cross compilers to use for C, C++ and Fortran +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++) +set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran) +set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres) + +# target environment on the build host system +set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) + +# modify default behavior of FIND_XXX() commands +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/codecov.yml b/codecov.yml index fee056bec5..cc26c5e8aa 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,2 +1,25 @@ -ignore: - - "xmpsdk" # Not interested about the coverage of XMKSDK +coverage: + precision: 2 + round: down + range: "60..100" + + # we don't care about the coverage of files in these folders + ignore: + - "xmpsdk" + - "unitTests" + - "samples" + + status: + project: + default: + threshold: 1 # decrease by up to 1% doesn't result in failure + branches: + - main + + # check only the diff of the PR + patch: + default: + threshold: 1 + branches: + - main + only_pulls: true diff --git a/conanfile.py b/conanfile.py index ad76b393fc..d6a1355ed6 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,10 +1,9 @@ from conans import ConanFile from conans.tools import os_info -from conans.model.version import Version class Exiv2Conan(ConanFile): settings = 'os', 'compiler', 'build_type', 'arch' - generators = 'cmake' + generators = 'cmake_find_package', 'cmake_paths' options = {'unitTests': [True, False], 'xmp': [True, False], 'iconv': [True, False], @@ -17,39 +16,32 @@ class Exiv2Conan(ConanFile): ) def configure(self): - self.options['libcurl'].shared = False - self.options['libcurl'].with_openssl = True - self.options['gtest'].shared = True + self.options['libcurl'].shared = True + self.options['gtest'].shared = False def requirements(self): - self.requires('zlib/1.2.11@conan/stable') + self.requires('zlib/1.3.1') + + self.requires('brotli/1.1.0') + + self.requires('inih/58') + + self.requires('fmt/11.0.2') + + if self.options.webready: + self.requires('libcurl/8.10.1') if os_info.is_windows and self.options.iconv: - self.requires('libiconv/1.15@bincrafters/stable') + self.requires('libiconv/1.17') if self.options.unitTests: - if self.settings.compiler == "Visual Studio" and Version(self.settings.compiler.version.value) <= "12": - self.requires('gtest/1.8.0@bincrafters/stable') - else: - self.requires('gtest/1.8.1@bincrafters/stable') - - if self.options.webready and not os_info.is_macos: - # Note: This difference in versions is just due to a combination of corner cases in the - # recipes and the OS & compiler versions used in Travis and AppVeyor. In normal cases we - # could use any of the versions.Also note that the issue was not with libcurl but with - # libopenssl (a transitive dependency) - if os_info.is_windows: - self.requires('libcurl/7.69.1') - self.options['libcurl'].with_openssl = False - self.options['libcurl'].with_winssl = True - else: - self.requires('libcurl/7.64.1@bincrafters/stable') + self.requires('gtest/1.15.0') if self.options.xmp: self.requires('XmpSdk/2016.7@piponazo/stable') # from conan-piponazo else: - self.requires('Expat/2.2.6@pix4d/stable') + self.requires('expat/2.6.3') def imports(self): - self.copy('*.dll', dst='conanDlls', src='bin') + self.copy('*.dll', dst='bin', src='bin') self.copy('*.dylib', dst='bin', src='lib') diff --git a/contrib/Qt/main.cpp b/contrib/Qt/main.cpp index 552f06f679..56217ca845 100644 --- a/contrib/Qt/main.cpp +++ b/contrib/Qt/main.cpp @@ -21,12 +21,13 @@ #include #include #include +#include int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); - exv_grep_keys_t keys; + std::vector keys; Exiv2::dumpLibraryInfo(std::cout,keys); return 0; diff --git a/contrib/coverity.sh b/contrib/coverity.sh index 9ceb4da3ad..29b35dfadd 100755 --- a/contrib/coverity.sh +++ b/contrib/coverity.sh @@ -7,7 +7,7 @@ # # Before to run this script you must set these shell variable : # $EXIVCoverityToken with token of Exiv2 project given by Coverity SCAN -# $EXIVCoverityEmail with email adress to send SCAN result. +# $EXIVCoverityEmail with email address to send SCAN result. # # Coverity Scan bin dir must be appended to PATH variable. # diff --git a/contrib/organize/MD5.cpp b/contrib/organize/MD5.cpp deleted file mode 100644 index a6e2e8223f..0000000000 --- a/contrib/organize/MD5.cpp +++ /dev/null @@ -1,235 +0,0 @@ -/* - * This code implements the MD5 message-digest algorithm. - * The algorithm is due to Ron Rivest. This code was - * written by Colin Plumb in 1993, no copyright is claimed. - * This code is in the public domain; do with it what you wish. - * - * Equivalent code is available from RSA Data Security, Inc. - * This code has been tested against that, and is equivalent, - * except that you don't need to include two pages of legalese - * with every copy. - * - * To compute the message digest of a chunk of bytes, declare an - * MD5_CTX structure, pass it to MD5Init, call MD5Update as - * needed on buffers full of bytes, and then call MD5Final, which - * will fill a supplied 16-byte array with the digest. - * - * Changed so as no longer to depend on Colin Plumb's `usual.h' header - * definitions; now uses stuff from dpkg's config.h. - * - Ian Jackson . - * Still in the public domain. - */ - -#include - -#include "MD5.h" - -using namespace std; - -static void -byteSwap(UWORD32 *buf, unsigned words) -{ - const uint32_t byteOrderTest = 0x1; - if (((char *)&byteOrderTest)[0] == 0) { - md5byte *p = (md5byte *)buf; - - do { - *buf++ = (UWORD32)((unsigned)p[3] << 8 | p[2]) << 16 | - ((unsigned)p[1] << 8 | p[0]); - p += 4; - } while (--words); - } -} - -/* - * Start MD5 accumulation. Set bit count to 0 and buffer to mysterious - * initialization constants. - */ -void -MD5Init(struct MD5_CTX *ctx) -{ - ctx->buf[0] = 0x67452301; - ctx->buf[1] = 0xefcdab89; - ctx->buf[2] = 0x98badcfe; - ctx->buf[3] = 0x10325476; - - ctx->bytes[0] = 0; - ctx->bytes[1] = 0; -} - -/* - * Update context to reflect the concatenation of another buffer full - * of bytes. - */ -void -MD5Update(struct MD5_CTX *ctx, md5byte const *buf, unsigned len) -{ - UWORD32 t; - - /* Update byte count */ - - t = ctx->bytes[0]; - if ((ctx->bytes[0] = t + len) < t) - ctx->bytes[1]++; /* Carry from low to high */ - - t = 64 - (t & 0x3f); /* Space available in ctx->in (at least 1) */ - if (t > len) { - memcpy((md5byte *)ctx->in + 64 - t, buf, len); - return; - } - /* First chunk is an odd size */ - memcpy((md5byte *)ctx->in + 64 - t, buf, t); - byteSwap(ctx->in, 16); - MD5Transform(ctx->buf, ctx->in); - buf += t; - len -= t; - - /* Process data in 64-byte chunks */ - while (len >= 64) { - memcpy(ctx->in, buf, 64); - byteSwap(ctx->in, 16); - MD5Transform(ctx->buf, ctx->in); - buf += 64; - len -= 64; - } - - /* Handle any remaining bytes of data. */ - memcpy(ctx->in, buf, len); -} - -/* - * Final wrapup - pad to 64-byte boundary with the bit pattern - * 1 0* (64-bit count of bits processed, MSB-first) - */ -void -MD5Final(md5byte digest[16], struct MD5_CTX *ctx) -{ - int count = ctx->bytes[0] & 0x3f; /* Number of bytes in ctx->in */ - md5byte *p = (md5byte *)ctx->in + count; - - /* Set the first char of padding to 0x80. There is always room. */ - *p++ = 0x80; - - /* Bytes of padding needed to make 56 bytes (-8..55) */ - count = 56 - 1 - count; - - if (count < 0) { /* Padding forces an extra block */ - memset(p, 0, count + 8); - byteSwap(ctx->in, 16); - MD5Transform(ctx->buf, ctx->in); - p = (md5byte *)ctx->in; - count = 56; - } - memset(p, 0, count); - byteSwap(ctx->in, 14); - - /* Append length in bits and transform */ - ctx->in[14] = ctx->bytes[0] << 3; - ctx->in[15] = ctx->bytes[1] << 3 | ctx->bytes[0] >> 29; - MD5Transform(ctx->buf, ctx->in); - - byteSwap(ctx->buf, 4); - memcpy(digest, ctx->buf, 16); - memset(ctx, 0, sizeof(ctx)); /* In case it's sensitive */ -} - -/* The four core functions - F1 is optimized somewhat */ - -/* #define F1(x, y, z) (x & y | ~x & z) */ -#define F1(x, y, z) (z ^ (x & (y ^ z))) -#define F2(x, y, z) F1(z, x, y) -#define F3(x, y, z) (x ^ y ^ z) -#define F4(x, y, z) (y ^ (x | ~z)) - -/* This is the central step in the MD5 algorithm. */ -#define MD5STEP(f,w,x,y,z,in,s) \ - (w += f(x,y,z) + in, w = (w<>(32-s)) + x) - -/* - * The core of the MD5 algorithm, this alters an existing MD5 hash to - * reflect the addition of 16 longwords of new data. MD5Update blocks - * the data and converts bytes into longwords for this routine. - */ -void -MD5Transform(UWORD32 buf[4], UWORD32 const in[16]) -{ - register UWORD32 a, b, c, d; - - a = buf[0]; - b = buf[1]; - c = buf[2]; - d = buf[3]; - - MD5STEP(F1, a, b, c, d, in[0] + 0xd76aa478, 7); - MD5STEP(F1, d, a, b, c, in[1] + 0xe8c7b756, 12); - MD5STEP(F1, c, d, a, b, in[2] + 0x242070db, 17); - MD5STEP(F1, b, c, d, a, in[3] + 0xc1bdceee, 22); - MD5STEP(F1, a, b, c, d, in[4] + 0xf57c0faf, 7); - MD5STEP(F1, d, a, b, c, in[5] + 0x4787c62a, 12); - MD5STEP(F1, c, d, a, b, in[6] + 0xa8304613, 17); - MD5STEP(F1, b, c, d, a, in[7] + 0xfd469501, 22); - MD5STEP(F1, a, b, c, d, in[8] + 0x698098d8, 7); - MD5STEP(F1, d, a, b, c, in[9] + 0x8b44f7af, 12); - MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17); - MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22); - MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7); - MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12); - MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17); - MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22); - - MD5STEP(F2, a, b, c, d, in[1] + 0xf61e2562, 5); - MD5STEP(F2, d, a, b, c, in[6] + 0xc040b340, 9); - MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14); - MD5STEP(F2, b, c, d, a, in[0] + 0xe9b6c7aa, 20); - MD5STEP(F2, a, b, c, d, in[5] + 0xd62f105d, 5); - MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9); - MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14); - MD5STEP(F2, b, c, d, a, in[4] + 0xe7d3fbc8, 20); - MD5STEP(F2, a, b, c, d, in[9] + 0x21e1cde6, 5); - MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9); - MD5STEP(F2, c, d, a, b, in[3] + 0xf4d50d87, 14); - MD5STEP(F2, b, c, d, a, in[8] + 0x455a14ed, 20); - MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5); - MD5STEP(F2, d, a, b, c, in[2] + 0xfcefa3f8, 9); - MD5STEP(F2, c, d, a, b, in[7] + 0x676f02d9, 14); - MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20); - - MD5STEP(F3, a, b, c, d, in[5] + 0xfffa3942, 4); - MD5STEP(F3, d, a, b, c, in[8] + 0x8771f681, 11); - MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16); - MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23); - MD5STEP(F3, a, b, c, d, in[1] + 0xa4beea44, 4); - MD5STEP(F3, d, a, b, c, in[4] + 0x4bdecfa9, 11); - MD5STEP(F3, c, d, a, b, in[7] + 0xf6bb4b60, 16); - MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23); - MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4); - MD5STEP(F3, d, a, b, c, in[0] + 0xeaa127fa, 11); - MD5STEP(F3, c, d, a, b, in[3] + 0xd4ef3085, 16); - MD5STEP(F3, b, c, d, a, in[6] + 0x04881d05, 23); - MD5STEP(F3, a, b, c, d, in[9] + 0xd9d4d039, 4); - MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11); - MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16); - MD5STEP(F3, b, c, d, a, in[2] + 0xc4ac5665, 23); - - MD5STEP(F4, a, b, c, d, in[0] + 0xf4292244, 6); - MD5STEP(F4, d, a, b, c, in[7] + 0x432aff97, 10); - MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15); - MD5STEP(F4, b, c, d, a, in[5] + 0xfc93a039, 21); - MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6); - MD5STEP(F4, d, a, b, c, in[3] + 0x8f0ccc92, 10); - MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15); - MD5STEP(F4, b, c, d, a, in[1] + 0x85845dd1, 21); - MD5STEP(F4, a, b, c, d, in[8] + 0x6fa87e4f, 6); - MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10); - MD5STEP(F4, c, d, a, b, in[6] + 0xa3014314, 15); - MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21); - MD5STEP(F4, a, b, c, d, in[4] + 0xf7537e82, 6); - MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10); - MD5STEP(F4, c, d, a, b, in[2] + 0x2ad7d2bb, 15); - MD5STEP(F4, b, c, d, a, in[9] + 0xeb86d391, 21); - - buf[0] += a; - buf[1] += b; - buf[2] += c; - buf[3] += d; -} diff --git a/contrib/organize/MD5.h b/contrib/organize/MD5.h deleted file mode 100644 index 00a5e94a5b..0000000000 --- a/contrib/organize/MD5.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef __MD5_h__ -#define __MD5_h__ - -/* - * This is the header file for the MD5 message-digest algorithm. - * The algorithm is due to Ron Rivest. This code was - * written by Colin Plumb in 1993, no copyright is claimed. - * This code is in the public domain; do with it what you wish. - * - * Equivalent code is available from RSA Data Security, Inc. - * This code has been tested against that, and is equivalent, - * except that you don't need to include two pages of legalese - * with every copy. - * - * To compute the message digest of a chunk of bytes, declare an - * MD5_CTX structure, pass it to MD5Init, call MD5Update as - * needed on buffers full of bytes, and then call MD5Final, which - * will fill a supplied 16-byte array with the digest. - * - * Changed so as no longer to depend on Colin Plumb's `usual.h' - * header definitions; now uses stuff from dpkg's config.h - * - Ian Jackson . - * Still in the public domain. - */ - -#include -#ifdef EXV_HAVE_STDINT_H -# include -#endif - -/* MSVC doesn't provide C99 types, but it has MS specific variants */ -#ifdef _MSC_VER -typedef unsigned __int32 uint32_t; -#endif - -typedef unsigned char md5byte; -typedef uint32_t UWORD32; - -struct MD5_CTX { - UWORD32 buf[4]; - UWORD32 bytes[2]; - UWORD32 in[16]; -}; - -extern void MD5Init(struct MD5_CTX *context); -extern void MD5Update(struct MD5_CTX *context, md5byte const *buf, unsigned len); -extern void MD5Final(unsigned char digest[16], struct MD5_CTX *context); -extern void MD5Transform(UWORD32 buf[4], UWORD32 const in[16]); - -#endif diff --git a/contrib/organize/Makefile b/contrib/organize/Makefile deleted file mode 100644 index cf4598060b..0000000000 --- a/contrib/organize/Makefile +++ /dev/null @@ -1,144 +0,0 @@ -# ************************************************************* -*- Makefile -*- -# -# Copyright (C) 2004-2015 Andreas Huggel -# -# This Makefile is part of the Exiv2 distribution. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# 3. The name of the author may not be used to endorse or promote -# products derived from this software without specific prior -# written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# File: Makefile -# Author(s): Andreas Huggel (ahu) -# History: 31-Jan-09, ahu: created -# -# Description: -# Simple Makefile to build the organize application. Requires installed -# exiv2 library and headers. Adapted from samples/Makefile. -# -# Restrictions: -# Requires GNU make. -# - -# ****************************************************************************** -# Default make target -all: ozbin - -# Include system configuration -top_srcdir = ../.. -include $(top_srcdir)/config/config.mk -include boost.mk - -# ****************************************************************************** -# Source files - -# Source files for the organize application -OZMAIN = organize.cpp -OZSRC = helpers.cpp MD5.cpp - -# ****************************************************************************** -# Initialisations -SHELL = /bin/sh - -.SUFFIXES: -.SUFFIXES: .c .cpp .o .so - -.PRECIOUS: %.cpp - -CPPFLAGS := -I$(BOOST_INC_DIR) `pkg-config exiv2 --cflags` -ifdef HAVE_STDINT - CPPFLAGS += -DEXV_HAVE_STDINT_H=1 -endif - -LDFLAGS := $(BOOST_LIBS) `pkg-config exiv2 --libs` - -OZOBJ = $(OZSRC:.cpp=.o) $(OZMAIN:.cpp=.o) -OZBIN = $(OZMAIN:.cpp=) -OZEXE = $(OZMAIN:.cpp=$(EXEEXT)) - -ifdef DEP_TRACKING -DEP = $(OZMAIN:%.cpp=$(DEPDIR)/%.d) $(OZSRC:%.cpp=$(DEPDIR)/%.d) -endif - -# ****************************************************************************** -# Rules -ozbin: $(OZBIN) - -$(OZOBJ): %.o: %.cpp - $(COMPILE.cc) -o $@ $< - @$(MAKEDEPEND) - @$(POSTDEPEND) - -%.ii: %.cpp - set -e; \ - $(CXXCPP) -E $(CPPFLAGS) $< | sed '/^[ ]*$$/d' > $@ - -# ****************************************************************************** -# Targets -.PHONY: all ozbin relink binclean install uninstall mostlyclean clean distclean maintainer-clean - -ifdef DEP_TRACKING -# Include targets from dependency files --include $(DEP) -endif - -$(OZBIN): $(OZOBJ) - $(LIBTOOL) --mode=link $(LINK.cc) -o $@ $(OZOBJ) - -relink: binclean organize - -install: - $(INSTALL_DIRS) $(DESTDIR)$(bindir) - @$(LIBTOOL) --mode=install $(INSTALL_PROGRAM) $(OZEXE) $(DESTDIR)$(bindir)/$(OZEXE) - -uninstall: - @$(LIBTOOL) --mode=uninstall $(RM) $(DESTDIR)$(bindir)/$(OZEXE) - -rmdir $(DESTDIR)$(bindir) - -# Remove binaries, e.g., to relink them -binclean: - $(RM) $(OZEXE) - -mostlyclean: - $(RM) core - $(RM) $(OZMAIN:.cpp=.ii) $(OZSRC:.cpp=.ii) - $(RM) $(OZMAIN:%.cpp=.libs/%.d) $(OZSRC:%.cpp=.libs/%.d) - -rmdir .libs - $(RM) $(OZOBJ) - -clean: binclean mostlyclean - -# Run `make distclean' from the top source directory to also remove -# files created by configuring the program. -distclean: clean -ifdef DEP_TRACKING - $(RM) $(DEP) - -rmdir $(DEPDIR) -endif - $(RM) *~ *.bak *# - -# This command is intended for maintainers to use; it deletes files -# that may need special tools to rebuild. -maintainer-clean: uninstall distclean diff --git a/contrib/organize/README b/contrib/organize/README deleted file mode 100644 index 26285f72d0..0000000000 --- a/contrib/organize/README +++ /dev/null @@ -1,3 +0,0 @@ -organize uses the Boost library (http://www.boost.org). -Configuration settings for Boost are in the file boost.mk -in this directory and should be changed as required. diff --git a/contrib/organize/boost.mk b/contrib/organize/boost.mk deleted file mode 100644 index b7cd50cb70..0000000000 --- a/contrib/organize/boost.mk +++ /dev/null @@ -1,3 +0,0 @@ -# Boost configuration for organize - change paths and library names as needed -BOOST_INC_DIR = /usr/local/include/boost-1_37 -BOOST_LIBS = /usr/local/lib/libboost_system-gcc43-mt-1_37.a /usr/local/lib/libboost_filesystem-gcc43-mt-1_37.a /usr/local/lib/libboost_regex-gcc43-mt-1_37.a /usr/local/lib/libboost_program_options-gcc43-mt-1_37.a diff --git a/contrib/organize/helpers.cpp b/contrib/organize/helpers.cpp deleted file mode 100644 index 18a5605f14..0000000000 --- a/contrib/organize/helpers.cpp +++ /dev/null @@ -1,635 +0,0 @@ -// ***************************************************************** -*- C++ -*- -/* - * Copyright (C) 2009 Brad Schick - * - * This file is part of the organize tool. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. - */ -// ***************************************************************************** - -#include -#include -#include -#include -#include -#include -#include -#include -#include -//#include -#include -#include -#include -#include "helpers.hpp" - -#define BOOST_FILESYSTEM_NO_DEPRECATED - -namespace fs = boost::filesystem; -typedef Exiv2::ExifData::const_iterator (*EasyAccessFct)(const Exiv2::ExifData& ed); - - -std::string scrub(const std::string &dirty, bool strip_space = false) -{ - std::string scrub = boost::trim_copy(dirty); - if(strip_space) { - boost::regex space("\\s"); - scrub = boost::regex_replace(scrub, space, ""); - } - boost::regex dash("[:/\\\\|<>]"); - boost::regex under("[\"'\\[\\]\\{\\}#=%\\$\\?,\\+\\*]"); - scrub = boost::regex_replace(scrub, dash, "-"); - - return boost::regex_replace(scrub, under, "_"); -} - -bool exif_data(const Exiv2::Image *image, const char *key, Exiv2::ExifData::const_iterator &md) -{ - assert(image && key); - bool ok = false; - try { - const Exiv2::ExifData &exifData = image->exifData(); - Exiv2::ExifKey exifKey(key); - md = exifData.findKey(exifKey); - if(md != exifData.end() && md->typeId() != Exiv2::undefined) - ok = true; - } - catch(const Exiv2::AnyError&) { - } - return ok; -} - -bool exif_data_easy(const Exiv2::Image *image, EasyAccessFct easy, Exiv2::ExifData::const_iterator &md) -{ - assert(image && easy); - bool ok = false; - try { - const Exiv2::ExifData &exifData = image->exifData(); - md = easy(exifData); - if(md != exifData.end() && md->typeId() != Exiv2::undefined) - ok = true; - } - catch(const Exiv2::AnyError&) { - } - return ok; -} - - -bool iptc_data(const Exiv2::Image *image, const char *key, Exiv2::IptcData::const_iterator &md) -{ - bool ok = false; - assert(image && key); - try { - const Exiv2::IptcData &iptcData = image->iptcData(); - Exiv2::IptcKey iptcKey(key); - md = iptcData.findKey(iptcKey); - if(md != iptcData.end() && md->typeId() != Exiv2::undefined) - ok = true; - } - catch(const Exiv2::AnyError&) { - } - return ok; -} - -std::string exif_date(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Photo.DateTimeDigitized", md); - if(!done) - done = exif_data(image, "Exif.Photo.DateTimeOriginal", md); - if(!done) - return ""; - - std::string date = scrub(md->print().substr(0,10)); - // Some files have zeros for dates, just fail in that case - if(boost::lexical_cast(date.substr(0,4))==0) - return ""; - - return date; -} - -std::string exif_year(const Exiv2::Image *image, const fs::path &path) -{ - std::string date = exif_date(image, path); - if(date.length()) - return date.substr(0,4); - else - return date; -} - -std::string exif_month(const Exiv2::Image *image, const fs::path &path) -{ - std::string date = exif_date(image, path); - if(date.length()) - return date.substr(5,2); - else - return date; -} - -std::string exif_day(const Exiv2::Image *image, const fs::path &path) -{ - std::string date = exif_date(image, path); - if(date.length()) - return date.substr(8,2); - else - return date; -} - -bool iptc_get_date(const Exiv2::Image *image, Exiv2::DateValue::Date &date) -{ - Exiv2::IptcData::const_iterator md; - bool done = iptc_data(image, "Iptc.Application2.DigitizationDate", md); - if(!done) - done = iptc_data(image, "Iptc.Application2.DateCreated", md); - if(!done) - return false; - date = ((Exiv2::DateValue*)md->getValue().get())->getDate(); - return date.year > 0; -} - -std::string iptc_date(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::DateValue::Date date; - if(iptc_get_date(image, date)) - return str(boost::format("%4d-%02d-%02d") % date.year % date.month % date.day); - else - return ""; -} - -std::string iptc_year(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::DateValue::Date date; - if(iptc_get_date(image, date)) - return str(boost::format("%4d") % date.year); - else - return ""; -} - -std::string iptc_month(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::DateValue::Date date; - if(iptc_get_date(image, date)) - return str(boost::format("%02d") % date.month); - else - return ""; -} - -std::string iptc_day(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::DateValue::Date date; - if(iptc_get_date(image, date)) - return str(boost::format("%02d") % date.day); - else - return ""; -} - -bool file_get_tm(const fs::path &path, std::tm &tm) -{ - std::time_t timer = fs::last_write_time(path); - if(time > 0) { - tm = *localtime(&timer); - return true; - } - else { - return false; - } -} - -std::string file_date(const Exiv2::Image *, const fs::path &path) -{ - std::tm tm; - if(file_get_tm(path, tm)) - return str(boost::format("%4d-%02d-%02d") % (tm.tm_year + 1900) % (tm.tm_mon + 1) % tm.tm_mday); - else - return ""; -} - -std::string file_year(const Exiv2::Image *, const fs::path &path) -{ - std::tm tm; - if(file_get_tm(path, tm)) - return str(boost::format("%4d") % (tm.tm_year + 1900)); - else - return ""; -} - -std::string file_month(const Exiv2::Image *, const fs::path &path) -{ - std::tm tm; - if(file_get_tm(path, tm)) - return str(boost::format("%02d") % (tm.tm_mon + 1)); - else - return ""; -} - -std::string file_day(const Exiv2::Image *, const fs::path &path) -{ - std::tm tm; - if(file_get_tm(path, tm)) - return str(boost::format("%02d") % tm.tm_mday); - else - return ""; -} - -/* -std::string xmp_date(const Exiv2::Image *image, const fs::path &) -{ - return ""; -} - -std::string xmp_year(const Exiv2::Image *image, const fs::path &) -{ - return ""; -} - -std::string xmp_month(const Exiv2::Image *image, const fs::path &) -{ - return ""; -} - -std::string xmp_day(const Exiv2::Image *image, const fs::path &) -{ - return ""; -}*/ - -std::string exif_time(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Photo.DateTimeDigitized", md); - if(!done) - done = exif_data(image, "Exif.Photo.DateTimeOriginal", md); - if(!done) - return ""; - - std::string datetime = md->print(); - // Some files have zeros for dates, just fail in that case - if(boost::lexical_cast(datetime.substr(0,4)) == 0) - return ""; - - return scrub(datetime.substr(11)); -} - -std::string exif_hour(const Exiv2::Image *image, const fs::path &path) -{ - std::string time = exif_time(image, path); - if(time.length()) - return time.substr(0,2); - else - return time; -} - -std::string exif_minute(const Exiv2::Image *image, const fs::path &path) -{ - std::string time = exif_time(image, path); - if(time.length()) - return time.substr(3,2); - else - return time; -} - -std::string exif_second(const Exiv2::Image *image, const fs::path &path) -{ - std::string time = exif_time(image, path); - if(time.length()) - return time.substr(6,2); - else - return time; -} - -bool iptc_get_time(const Exiv2::Image *image, Exiv2::TimeValue::Time &time) -{ - Exiv2::IptcData::const_iterator md; - bool done = iptc_data(image, "Iptc.Application2.DigitizationTime", md); - if(!done) - done = iptc_data(image, "Iptc.Application2.TimeCreated", md); - if(!done) - return false; - time = ((Exiv2::TimeValue*)md->getValue().get())->getTime(); - // Zero is a valid time, so this one is hard to check. - return true; -} - -std::string iptc_time(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::TimeValue::Time time; - if(iptc_get_time(image, time)) - return str(boost::format("%02d-%02d-%02d") % time.hour % time.minute % time.second); - else - return ""; -} - -std::string iptc_hour(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::TimeValue::Time time; - if(iptc_get_time(image, time)) - return str(boost::format("%02d") % time.hour); - else - return ""; -} - -std::string iptc_minute(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::TimeValue::Time time; - if(iptc_get_time(image, time)) - return str(boost::format("%02d") % time.minute); - else - return ""; -} - -std::string iptc_second(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::TimeValue::Time time; - if(iptc_get_time(image, time)) - return str(boost::format("%02d") % time.second); - else - return ""; -} - -std::string file_time(const Exiv2::Image *, const fs::path &path) -{ - std::tm tm; - if(file_get_tm(path, tm)) - return str(boost::format("%02d-%02d-%02d") % tm.tm_hour % tm.tm_min % tm.tm_sec); - else - return ""; -} - -std::string file_hour(const Exiv2::Image *, const fs::path &path) -{ - std::tm tm; - if(file_get_tm(path, tm)) - return str(boost::format("%02d") % tm.tm_hour); - else - return ""; -} - -std::string file_minute(const Exiv2::Image *, const fs::path &path) -{ - std::tm tm; - if(file_get_tm(path, tm)) - return str(boost::format("%02d") % tm.tm_min); - else - return ""; -} - -std::string file_second(const Exiv2::Image *, const fs::path &path) -{ - std::tm tm; - if(file_get_tm(path, tm)) - return str(boost::format("%02d") % tm.tm_sec); - else - return ""; -} - -/*std::string xmp_time(const Exiv2::Image *image, const fs::path &) -{ - return ""; -} - -std::string xmp_hour(const Exiv2::Image *image, const fs::path &) -{ - return ""; -} - -std::string xmp_minute(const Exiv2::Image *image, const fs::path &) -{ - return ""; -} - -std::string xmp_second(const Exiv2::Image *image, const fs::path &) -{ - return ""; -}*/ - -std::string exif_dimension(const Exiv2::Image *image, const fs::path &path) -{ - return exif_width(image, path) + "-" + exif_height(image, path); -} - -std::string exif_width(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Photo.PixelXDimension", md); - if(!done) - return ""; - return scrub(md->print()); -} - -std::string exif_height(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Photo.PixelYDimension", md); - if(!done) - return ""; - return scrub(md->print()); -} - -std::string file_dimension(const Exiv2::Image *image, const fs::path &path) -{ - if(image) - return file_width(image, path) + "-" + file_height(image, path); - else - return ""; -} - -std::string file_width(const Exiv2::Image *image, const fs::path &) -{ - if(image) - return str(boost::format("%02d") % image->pixelWidth()); - else - return ""; -} - -std::string file_height(const Exiv2::Image *image, const fs::path &) -{ - if(image) - return str(boost::format("%02d") % image->pixelHeight()); - else - return ""; -} - -/* -std::string xmp_dimension(const Exiv2::Image *image, const fs::path &) -{ - return "" -} - -std::string xmp_width(const Exiv2::Image *image, const fs::path &) -{ - return ""; -} - -std::string xmp_height(const Exiv2::Image *image, const fs::path &) -{ - return ""; -}*/ - -std::string exif_model(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Image.Model", md); - if(!done) - return ""; - return scrub(md->print()); -} - -std::string exif_make(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Image.Make", md); - if(!done) - return ""; - return scrub(md->print()); -} - -/*std::string xmp_model(const Exiv2::Image *image, const fs::path &) -{ - return ""; -}*/ - -std::string exif_speed(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Photo.ShutterSpeedValue", md); - if(!done) - done = exif_data(image, "Exif.Photo.ExposureTime", md); - if(!done) - return ""; - return scrub(md->print()); -} - -/*std::string xmp_speed(const Exiv2::Image *image, const fs::path &) -{ - return ""; -}*/ - -std::string exif_aperture(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Photo.ApertureValue", md); - if(!done) - done = exif_data(image, "Exif.Photo.FNumber", md); - if(!done) - return ""; - return scrub(md->print()); -} - -/*std::string xmp_aperture(const Exiv2::Image *image, const fs::path &) -{ - return ""; -}*/ - -std::string exif_focal(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Photo.FocalLength", md); - if(!done) - return ""; - return scrub(md->print()); -} - -/*std::string xmp_focal(const Exiv2::Image *image, const fs::path &) -{ - return ""; -}*/ - -std::string exif_distance(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Photo.SubjectDistance", md); - if(!done) - return ""; - return scrub(md->print()); -} - -/*std::string xmp_distance(const Exiv2::Image *image, const fs::path &) -{ - return ""; -}*/ - -std::string exif_meter(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Photo.MeteringMode", md); - if(!done) - return ""; - return scrub(md->print()); -} - -std::string exif_macro(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data_easy(image, Exiv2::macroMode, md); - if(!done) - return ""; - return scrub(md->print()); -} - -std::string exif_orientation(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data_easy(image, Exiv2::orientation, md); - if(!done) - return ""; - return scrub(md->print(), true); -} - -std::string exif_lens(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data_easy(image, Exiv2::lensName, md); - if(!done) - return ""; - return scrub(md->print()); -} - - -std::string exif_iso(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data_easy(image, Exiv2::isoSpeed, md); - if(!done) - return ""; - return scrub(md->print()); -} - -/*std::string xmp_meter(const Exiv2::Image *image, const fs::path &) -{ - return ""; -}*/ - -std::string exif_keyword(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::ExifData::const_iterator md; - bool done = exif_data(image, "Exif.Photo.UserComment", md); - if(!done) - return ""; - return scrub(md->print()); -} - -std::string iptc_keyword(const Exiv2::Image *image, const fs::path &) -{ - Exiv2::IptcData::const_iterator md; - bool done = iptc_data(image, "Iptc.Application2.Keywords", md); - if(!done) - return ""; - return scrub(md->print()); -} - -/*std::string xmp_keyword(const Exiv2::Image *image, const fs::path &) -{ - return ""; -}*/ - diff --git a/contrib/organize/helpers.hpp b/contrib/organize/helpers.hpp deleted file mode 100644 index 5a85e28bdb..0000000000 --- a/contrib/organize/helpers.hpp +++ /dev/null @@ -1,101 +0,0 @@ -// ***************************************************************** -*- C++ -*- -/* - * Copyright (C) 2004-2021 Exiv2 authors - * This program is part of the Exiv2 distribution. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. - */ -// ***************************************************************************** - - -#ifndef HELPERS_HPP_ -#define HELPERS_HPP_ - -#include - -#define BOOST_FILESYSTEM_NO_DEPRECATED -namespace fs = boost::filesystem; - - -typedef std::string (*pfunc)(const Exiv2::Image *image, const fs::path &path); - -// This would be a lot smaller if Exiv2 had support -// for unified metadata - -std::string exif_date(const Exiv2::Image *image, const fs::path &path); -std::string exif_year(const Exiv2::Image *image, const fs::path &path); -std::string exif_month(const Exiv2::Image *image, const fs::path &path); -std::string exif_day(const Exiv2::Image *image, const fs::path &path); -std::string iptc_date(const Exiv2::Image *image, const fs::path &path); -std::string iptc_year(const Exiv2::Image *image, const fs::path &path); -std::string iptc_month(const Exiv2::Image *image, const fs::path &path); -std::string iptc_day(const Exiv2::Image *image, const fs::path &path); -std::string file_date(const Exiv2::Image *image, const fs::path &path); -std::string file_year(const Exiv2::Image *image, const fs::path &path); -std::string file_month(const Exiv2::Image *image, const fs::path &path); -std::string file_day(const Exiv2::Image *image, const fs::path &path); -/*std::string xmp_date(const Exiv2::Image *image, const fs::path &path); -std::string xmp_year(const Exiv2::Image *image, const fs::path &path); -std::string xmp_month(const Exiv2::Image *image, const fs::path &path); -std::string xmp_day(const Exiv2::Image *image, const fs::path &path);*/ -std::string exif_time(const Exiv2::Image *image, const fs::path &path); -std::string exif_hour(const Exiv2::Image *image, const fs::path &path); -std::string exif_minute(const Exiv2::Image *image, const fs::path &path); -std::string exif_second(const Exiv2::Image *image, const fs::path &path); -std::string iptc_time(const Exiv2::Image *image, const fs::path &path); -std::string iptc_hour(const Exiv2::Image *image, const fs::path &path); -std::string iptc_minute(const Exiv2::Image *image, const fs::path &path); -std::string iptc_second(const Exiv2::Image *image, const fs::path &path); -std::string file_time(const Exiv2::Image *image, const fs::path &path); -std::string file_hour(const Exiv2::Image *image, const fs::path &path); -std::string file_minute(const Exiv2::Image *image, const fs::path &path); -std::string file_second(const Exiv2::Image *image, const fs::path &path); -/*std::string xmp_time(const Exiv2::Image *image, const fs::path &path); -std::string xmp_hour(const Exiv2::Image *image, const fs::path &path); -std::string xmp_minute(const Exiv2::Image *image, const fs::path &path); -std::string xmp_second(const Exiv2::Image *image, const fs::path &path);*/ -std::string exif_dimension(const Exiv2::Image *image, const fs::path &path); -std::string exif_width(const Exiv2::Image *image, const fs::path &path); -std::string exif_height(const Exiv2::Image *image, const fs::path &path); -std::string file_dimension(const Exiv2::Image *image, const fs::path &path); -std::string file_width(const Exiv2::Image *image, const fs::path &path); -std::string file_height(const Exiv2::Image *image, const fs::path &path); -/*std::string xmp_dimension(const Exiv2::Image *image, const fs::path &path); -std::string xmp_width(const Exiv2::Image *image, const fs::path &path); -std::string xmp_height(const Exiv2::Image *image, const fs::path &path);*/ -std::string exif_model(const Exiv2::Image *image, const fs::path &path); -std::string exif_make(const Exiv2::Image *image, const fs::path &path); -/*std::string xmp_model(const Exiv2::Image *image, const fs::path &path); -std::string xmp_make(const Exiv2::Image *image, const fs::path &path);*/ -std::string exif_speed(const Exiv2::Image *image, const fs::path &path); -//std::string xmp_speed(const Exiv2::Image *image, const fs::path &path); -std::string exif_aperture(const Exiv2::Image *image, const fs::path &path); -//std::string xmp_aperture(const Exiv2::Image *image, const fs::path &path); -std::string exif_focal(const Exiv2::Image *image, const fs::path &path); -//std::string xmp_focal(const Exiv2::Image *image, const fs::path &path); -std::string exif_distance(const Exiv2::Image *image, const fs::path &path); -//std::string xmp_distance(const Exiv2::Image *image, const fs::path &path); -std::string exif_meter(const Exiv2::Image *image, const fs::path &path); -//std::string xmp_meter(const Exiv2::Image *image, const fs::path &path); -std::string exif_macro(const Exiv2::Image *image, const fs::path &path); -std::string exif_orientation(const Exiv2::Image *image, const fs::path &path); -std::string exif_lens(const Exiv2::Image *image, const fs::path &path); -std::string exif_keyword(const Exiv2::Image *image, const fs::path &path); -std::string iptc_keyword(const Exiv2::Image *image, const fs::path &path); -//std::string xmp_keyword(const Exiv2::Image *image, const fs::path &path); -std::string exif_iso(const Exiv2::Image *image, const fs::path &path); - -#endif //HELPERS_HPP_ - diff --git a/contrib/organize/organize.cpp b/contrib/organize/organize.cpp deleted file mode 100644 index a176a6bc01..0000000000 --- a/contrib/organize/organize.cpp +++ /dev/null @@ -1,759 +0,0 @@ -// ***************************************************************** -*- C++ -*- -/* - * Copyright (C) 2004-2021 Exiv2 authors - * This program is part of the Exiv2 distribution. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. - */ -// ***************************************************************************** - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "MD5.h" -#include "helpers.hpp" - -typedef Exiv2::byte md5digest[16]; - -namespace po = boost::program_options; - -bool g_verbose = false; -bool g_neednewline = false; - -// Array size should match number of SLOTs -boost::array g_run_order = {{-1, -1, -1, -1}}; -const int EXIF_SLOT = 0; -const int IPTC_SLOT = 1; -const int XMP_SLOT = 2; -const int FILE_SLOT = 3; - -const unsigned DOT_EVERY = 55; - -struct Pattern { - std::string pat; - std::string desc; - pfunc funcs[4]; // order should always be exif, iptc, xmp, file -}; - -struct PathPart { - std::string pre; - const Pattern *pat; - std::string post; - PathPart(std::string pre_, const Pattern *pat_, std::string post_) - : pre(pre_), pat(pat_), post(post_) {} -}; - -std::vector g_path_parts; - -// Instead of making these all global -struct ProcessParams { - const fs::path &dest_dir; - const bool dry_run; - const bool ignore_dups; - const bool ignore_unsorted; - const bool force; - const bool rename; - const bool symlink; - const bool verify; - const bool move; - const long limit_depth; - const fs::path &dups_dir; - const fs::path &unsorted_dir; - const std::vector &excludes; - unsigned dups_count; - unsigned unsorted_count; - unsigned dir_err_count; - unsigned file_err_count; - unsigned ok_count; - unsigned dups_ignored_count; - unsigned unsorted_ignored_count; - unsigned dir_ex_count; - unsigned file_ex_count; -}; - -void process_directory(const fs::path &directory, const long depth, - ProcessParams ¶ms); - -const Pattern g_patterns[] = { - {"@date", "date captured (2009-01-19)", - {exif_date, iptc_date, NULL, file_date} }, - {"@year", "year captured (2009)", - {exif_year, iptc_year, NULL, file_year} }, - {"@month", "month captured (01)", - {exif_month, iptc_month, NULL, file_month} }, - {"@day", "day captured (19)", - {exif_day, iptc_day, NULL, file_day} }, - {"@time", "time captured (14-35-27)", - {exif_time, iptc_time, NULL, file_time} }, - {"@hour", "hour captured (14)", - {exif_hour, iptc_hour, NULL, file_hour} }, - {"@min", "minute captured (35)", - {exif_minute, iptc_minute, NULL, file_minute} }, - {"@sec", "second captured (27)", - {exif_second, iptc_second, NULL, file_second} }, - {"@dim", "pixel dimension (2272-1704)", - {exif_dimension, NULL, NULL, file_dimension} }, - {"@x", "pixel width (2272)", - {exif_width, NULL, NULL, file_width} }, - {"@y", "pixel height (1704)", - {exif_height, NULL, NULL, file_height} }, - {"@make", "device make (Canon)", - {exif_make, NULL, NULL, NULL} }, - {"@model", "device model (Canon PowerShot S40)", - {exif_model, NULL, NULL, NULL} }, - {"@speed", "shutter speed (1-60)", - {exif_speed, NULL, NULL, NULL} }, - {"@aper", "aperture (F3.2)", - {exif_aperture, NULL, NULL, NULL} }, - {"@iso", "iso speed (400)", - {exif_iso, NULL, NULL, NULL} }, - {"@focal", "focal length (8.6 mm)", - {exif_focal, NULL, NULL, NULL} }, - {"@dist", "subject distance (1.03 m)", - {exif_distance, NULL, NULL, NULL} }, - {"@meter", "meter mode (multi-segment)", - {exif_meter, NULL, NULL, NULL} }, - {"@macro", "macro mode (Off)", - {exif_macro, NULL, NULL, NULL} }, - {"@orient", "orientation (top_left)", - {exif_orientation, NULL, NULL, NULL} }, - {"@lens", "lens name (Tamron 90mm f-2.8)", - {exif_lens, NULL, NULL, NULL} }, - {"@key", "first keyword (Family)", - {exif_keyword, iptc_keyword, NULL, NULL} }, - - {"", "", {NULL, NULL, NULL, NULL} } -}; - - -// Check that 'opt1' and 'opt2' are not specified at the same time. -void conflicting(const po::variables_map& vm, - const char* opt1, const char* opt2) -{ - if (vm.count(opt1) && !vm[opt1].defaulted() - && vm.count(opt2) && !vm[opt2].defaulted()) { - throw std::logic_error(std::string("conflicting options '") - + opt1 + "' and '" + opt2 + "'"); - } -} - -// Check that 'required' is present -void required(const po::variables_map& vm, const char* required) -{ - if (!vm.count(required) || vm[required].defaulted()) { - throw std::logic_error(std::string("required parameter '") + required - + "' is missing"); - } -} - -void info(const std::string &msg) -{ - if(g_verbose) { - std::cout << msg << "\n"; - g_neednewline = false; - } -} - -void error(const std::exception &e, const std::string &msg) -{ - if(g_neednewline) { - std::cout << "\n"; - g_neednewline = false; - } - std::cerr << e.what() << "\n"; - std::cerr << msg << std::endl; -} - -void usage_header(const char* exname) -{ - std::cout << "Usage: " << exname << " [options] source-dir dest-dir pattern\n"; -} - -void usage_full(const po::options_description &options, const char* exname) -{ - usage_header(exname); - std::cout << "\n Creates groups of files in new directories defined by a metadata 'pattern'.\n" << - " Files are copied, moved, or linked from 'source-dir' to 'dest-dir'.\n" << - " The destination directory should not be within the source directory.\n\n"; - std::cout << options; - - std::cout << "\nPattern values:\n"; - for( const Pattern *pattern = g_patterns; pattern->pat.length(); ++pattern) { - std::cout << " " << std::setw(8) << std::left << pattern->pat; - std::cout << pattern->desc << "\n"; - } - - std::cout << "\nExamples:\n"; - std::cout << " `" << exname << " -m mess clean @year-@month'\n"; - std::cout << " Moves files from 'mess' into directories of 'clean' according to\n" << - " year-month the file was captured (clean/2006-11/...)\n\n"; - std::cout << " `" << exname << " -o ie source find width-@x/height-@y'\n"; - std::cout << " Copies files into directories according first to pixel width then pixel\n" << - " height. Check iptc then exif metadata (find/width-2272/height-1704/...)\n\n"; - std::cout << " `" << exname << " -lf source find @aper/@hour'\n"; - std::cout << " Force create symlinks in directories according first to aperture then\n" << - " hour captured (find/F3.2/15/...)\n"; - - std::cout << std::endl; -} - -void version() -{ - std::cout << "organized 0.1\n" << - "Copyright (C) 2009 Brad Schick. \n\n" << - "This program is free software; you can redistribute it and/or\n" - "modify it under the terms of the GNU General Public License\n" - "as published by the Free Software Foundation; either version 2\n" - "of the License, or (at your option) any later version.\n" - "\n" - "This program is distributed in the hope that it will be useful,\n" - "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" - "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" - "GNU General Public License for more details.\n" - "\n" - "You should have received a copy of the GNU General Public\n" - "License along with this program; if not, write to the Free\n" - "Software Foundation, Inc., 51 Franklin Street, Fifth Floor,\n" - "Boston, MA 02110-1301 USA" << std::endl; -} - -// Returns empty string if the destination subdirectory could not be determined -// for the supplied source file. -std::string build_dest(const fs::path &source_file) -{ - std::string dest; - - Exiv2::Image::AutoPtr image; - try { - image = Exiv2::ImageFactory::open(source_file.string()); - image->readMetadata(); - } - catch(const Exiv2::AnyError&) { - // No metadata, let things continue to try file info - } - - std::vector::iterator iter = g_path_parts.begin(); - std::vector::iterator end = g_path_parts.end(); - for( ; iter != end; ++iter) { - dest += iter->pre; - std::string result; - - const Pattern *pat = iter->pat; - for(unsigned fx = 0; fx < g_run_order.size(); ++fx) { - if(g_run_order[fx] != -1 && pat->funcs[g_run_order[fx]]) { - if(g_run_order[fx] == FILE_SLOT) { - // Always run file operations - result = pat->funcs[g_run_order[fx]](image.get(), source_file); - } - else if(image.get()) { - // No point in running metadata operations without an image - result = pat->funcs[g_run_order[fx]](image.get(), source_file); - } - if(result.length()) - break; - } - } - // If we found no data, even for part of pattern, give up and - // return no destination - if(!result.length()) - return result; - - dest += (result + iter->post); - } - return dest; -} - -bool md5sum(const fs::path &path, md5digest &digest) -{ - try { - Exiv2::FileIo io(path.string()); - if (io.open() != 0) - return false; - Exiv2::IoCloser closer(io); - - Exiv2::byte buff[4096]; - MD5_CTX context; - MD5Init(&context); - - long read_count = io.read(buff, 4096); - while(read_count) { - MD5Update(&context, buff, read_count); - read_count = io.read(buff, 4096); - } - MD5Final(digest, &context); - return true; - } - catch (std::exception& ) { - return false; - } -} - - -int main(int argc, char* argv[]) -{ - po::options_description options("Options"); - // Don't use default values because the help print it ugly and too wide - options.add_options() - ("move,m", "move files rather than copy") - ("symlink,s", "symlink files rather than copy (posix only)") - ("order,o", po::value(), - "order and types of metadata to read\ne=exif, i=iptc, f=file (default: eif)") - ("unsorted,u", po::value(), - "special directory to store unsorted files (default: unsorted)") - ("dups,d", po::value(), - "special directory to store files with duplicate names (default: duplicates)") - ("force,f", "overwrite duplicate files instead of using special directory") - ("rename,r", "rename duplicate files instead of using special directory") - ("ignore,i", "ignore both unsorted and duplicate files instead of using special directories") - ("ignore-unsorted", "ignore unsorted files instead of using special directory") - ("ignore-dups", "ignore duplicate files instead of using special directory") - ("verify", "verify copied or moved files and exit if incorrect") - ("exclude,x", po::value< std::vector >(), - "exclude directories and files that contain arg (case sensitive on all platforms)") - ("limit-depth,l", po::value(), - "limit recursion to specified depth (0 disables recursion)") - ("verbose,v", "prints operations as they happen") - ("dry-run,n", "do not make actual changes (implies verbose)") - ("help,h", "show this help message then exit") - ("version,V", "show program version then exit") - ; - - po::options_description hidden("Hidden Options"); - hidden.add_options() - ("source-dir", po::value< std::string >(), "directory of files to organize, may end in file wildcard") - ("dest-dir", po::value< std::string >(), "designation directory for files, may not be within source-dir") - ("pattern", po::value< std::string >(), "subdirectory pattern for grouping files within dest-dir") - ; - - po::options_description cmdline; - cmdline.add(options).add(hidden); - - po::positional_options_description positional; - positional.add("source-dir", 1); - positional.add("dest-dir", 1); - positional.add("pattern", 1); - - try { - po::variables_map vm; - po::store(po::command_line_parser(argc, argv). - options(cmdline).positional(positional).run(), vm); - po::notify(vm); - - if (vm.count("help")) { - usage_full(options, argv[0]); - return 0; - } - - if (vm.count("version")) { - version(); - return 0; - } - - conflicting(vm, "verify", "symlink"); - conflicting(vm, "move", "symlink"); - conflicting(vm, "unsorted", "ignore"); - conflicting(vm, "unsorted", "ignore-unsorted"); - conflicting(vm, "dups", "ignore"); - conflicting(vm, "dups", "ignore-dups"); - conflicting(vm, "force", "ignore"); - conflicting(vm, "force", "ignore-dups"); - conflicting(vm, "force", "rename"); - conflicting(vm, "rename", "ignore"); - conflicting(vm, "rename", "ignore-dups"); - required(vm, "source-dir"); - required(vm, "dest-dir"); - required(vm, "pattern"); - - const bool dry_run = vm.count("dry-run") != 0; - g_verbose = (vm.count("verbose") != 0 || dry_run); - - std::string order = "eif"; - if(vm.count("order")) { - order = vm["order"].as(); - - boost::to_lower(order); - if(order.length() > 3) { - throw std::logic_error(std::string("order is longer than 4 characters")); - } - } - - unsigned i = 0; - std::string::iterator end = order.end(); - for(std::string::iterator iter = order.begin(); iter != end && i < 4; ++iter, ++i) { - switch(*iter) { - case 'e': - g_run_order[i] = EXIF_SLOT; - break; - case 'i': - g_run_order[i] = IPTC_SLOT; - break; - case 'x': - throw std::logic_error(std::string("xmp not implemented yet '") + - *iter + "'"); - break; - case 'f': - g_run_order[i] = FILE_SLOT; - break; - default: - throw std::logic_error(std::string("unknown order character '") + - *iter + "'"); - } - } - - const fs::path source_dir( vm["source-dir"].as() ); - if( !exists(source_dir) || !is_directory(source_dir) ) { - throw std::logic_error(std::string("source '") + - source_dir.string() + "' must exist and be a directory"); - } - - const fs::path dest_dir( vm["dest-dir"].as() ); - if( exists(dest_dir) && !is_directory(dest_dir) ) { - throw std::logic_error(std::string("destination '") + - dest_dir.string() + "' must be a directory"); - } - - // Boost doesn't seem to have a way to get a canonical path, so this - // simple test is easy to confuse with some ../../'s in the paths. Oh - // well, this is good enough for now. - fs::path test_dest(dest_dir); - for(; !test_dest.empty(); test_dest = test_dest.parent_path()) { - if(fs::equivalent(source_dir, test_dest)) { - throw std::logic_error(std::string("dest-dir must not be within source-dir")); - } - } - - // Disect the pattern - std::string pattern = vm["pattern"].as(); - boost::regex regex( "([^@]*)(@[[:alpha:]]+)([^@]*)"); - boost::sregex_iterator m_iter = make_regex_iterator(pattern, regex); - boost::sregex_iterator m_end; - for( ; m_iter != m_end; ++m_iter) { - const boost::smatch &match = *m_iter; - const std::string &pre = match[1]; - const std::string &pat = match[2]; - const std::string &post = match[3]; - - // Should put this in a map, but there aren't that many options now - bool found = false; - for( const Pattern *pattern = g_patterns; pattern->pat.length(); ++pattern) { - if(pattern->pat == pat) { - PathPart part(pre, pattern, post); - g_path_parts.push_back(part); - found = true; - break; - } - } - - if(!found) { - throw std::logic_error(std::string("unknown pattern '") + pat + "'"); - } - } - - // Assign defaults to params that need them - const bool ignore = vm.count("ignore") != 0; - std::vector excludes; - if(vm.count("exclude")) - excludes = vm["exclude"].as< std::vector >(); - long limit_depth = LONG_MAX; - if(vm.count("limit-depth")) { - limit_depth = vm["limit-depth"].as(); - // Boost program_options doesn't work with unsigned, so do it manually - if( limit_depth < 0 ) - throw std::logic_error(std::string("recursion depth limit must be positive")); - } - std::string dups = "duplicates"; - if(vm.count("dups")) - dups = vm["dups"].as(); - const fs::path dups_dir = dest_dir / dups; - - std::string unsorted = "unsorted"; - if(vm.count("unsorted")) - unsorted = vm["unsorted"].as(); - const fs::path unsorted_dir = dest_dir / unsorted; - - ProcessParams params = { - dest_dir, - dry_run, - (vm.count("ignore-dups") != 0 || ignore), - (vm.count("ignore-unsorted") != 0 || ignore), - vm.count("force") != 0, - vm.count("rename") != 0, - vm.count("symlink") != 0, - vm.count("verify") != 0, - vm.count("move") != 0, - limit_depth, - dups_dir, - unsorted_dir, - excludes, - 0, 0, 0, 0, 0, 0, 0, 0, 0 - }; - - process_directory(source_dir, 0, params); - - std::string op = "copied"; - if(params.symlink) - op = "linked"; - else if(params.move) - op = "moved"; - - if(dry_run) - op = std::string("would be ") + op; - - if(g_neednewline) - std::cout << "\n"; - - std::cout << "\n" << params.ok_count << " files " << op << "\n"; - std::cout << " " << params.dups_count << " duplicates\n"; - std::cout << " " << params.unsorted_count << " unsorted\n"; - if(params.dups_ignored_count) - std::cout << params.dups_ignored_count << " duplicates ignored\n"; - if(params.unsorted_ignored_count) - std::cout << params.unsorted_ignored_count << " unsorted ignored\n"; - if(params.dir_ex_count) - std::cout << params.dir_ex_count << " directories excluded\n"; - if(params.file_ex_count) - std::cout << params.file_ex_count << " files excluded\n"; - if(params.dir_err_count) - std::cout << params.dir_err_count << " directory errors\n"; - if(params.file_err_count) - std::cout << params.file_err_count << " file errors\n"; - - return 0; - } - catch (Exiv2::AnyError& e) { - error(e, std::string("Aborting")); - return -1; - } - catch(std::logic_error& e) { - error(e, ""); - usage_header(argv[0]); - std::cout << argv[0] << " -h for more help" << std::endl; - return -2; - } - catch(std::exception& e) { - error(e, "Aborting"); - return -3; - } -} - -boost::regex uregex("(.*?)\\(([[:digit:]]{1,2})\\)$"); - -fs::path uniquify(const fs::path &dest) -{ - std::string ext = dest.extension().string(); - std::string fname = dest.stem().string(); - fs::path parent = dest.parent_path(); - - unsigned number = 1; - std::string newfname; - fs::path newdest; - - boost::smatch match; - if(boost::regex_search(fname, match, uregex)) { - // Matches are indexes into fname, so don't change it while reading values - newfname = match[1]; - number = boost::lexical_cast(match[2]); - fname = newfname; - } - - do { - newfname = fname + "(" + boost::lexical_cast(++number) + ")" + ext; - newdest = parent / newfname; - } while(fs::exists(newdest)); - - return newdest; -} - -void process_directory(const fs::path &directory, const long depth, - ProcessParams ¶ms) -{ - // Exclude entire directories - bool exclude = false; - std::vector::const_iterator x_iter = params.excludes.begin(); - std::vector::const_iterator x_end = params.excludes.end(); - for( ; x_iter != x_end; ++x_iter ) { - if(boost::contains(directory.string(), *x_iter)) { - exclude = true; - break; - } - } - if(exclude) { - info(std::string("excluding directory: ") + directory.string() + - " matched: " + *x_iter); - ++params.dir_ex_count; - return; - } - - try { - fs::directory_iterator p_iter(directory), p_end; - for( ; p_iter != p_end; ++p_iter) { - if( is_directory(*p_iter) ) { - // recurse if we haven't hit the limit - if(depth < params.limit_depth) - process_directory(p_iter->path(), depth + 1, params); - else { - info(std::string("depth reached, skipping: ") + - p_iter->path().string()); - } - } - else if( is_regular_file(*p_iter) ) { - - // Check again for excluding file names - exclude = false; - x_iter = params.excludes.begin(); - for( ; x_iter != x_end; ++x_iter ) { - if(boost::contains(p_iter->path().string(), *x_iter)) { - exclude = true; - break; - } - } - if(exclude) { - info(std::string("excluding file: ") + p_iter->path().string() + - " matched: " + *x_iter); - ++params.file_ex_count; - continue; - } - - try { - const fs::path dest_subdir = build_dest(*p_iter); - fs::path dest_file; - if(!dest_subdir.empty()) - dest_file = params.dest_dir / dest_subdir; - else if(params.ignore_unsorted) { - info(std::string("ignoring unsorted: ") + p_iter->path().string()); - ++params.unsorted_ignored_count; - continue; - } - else { - info(std::string("unsorted file (missing metadata): ") + p_iter->path().string()); - dest_file = params.unsorted_dir; - ++params.unsorted_count; - } - - dest_file /= p_iter->path().filename(); - - if(fs::exists(dest_file)) { - if(params.ignore_dups) { - info(std::string("ignoring: ") + p_iter->path().string() + - " duplicates: " + dest_file.string()); - ++params.dups_ignored_count; - continue; - } - else { - if(params.force) { - info(std::string("force removing: ") + dest_file.string() + " for: " - + p_iter->path().string()); - if(!params.dry_run) - fs::remove(dest_file); - } - else if(params.rename) { - info(std::string("renaming: ") + p_iter->path().string() + - " duplicates: " + dest_file.string()); - dest_file = uniquify(dest_file); - } - else { - info(std::string("duplicate file: ") + p_iter->path().string() + - " of: " + dest_file.string()); - dest_file = params.dups_dir / dest_subdir / p_iter->path().filename(); - // Ugh, more dup possibilities - if(fs::exists(dest_file)) { - info(std::string("renaming: ") + p_iter->path().string() + - " duplicates: " + dest_file.string()); - dest_file = uniquify(dest_file); - } - } - ++params.dups_count; - } - } - - if(!params.dry_run) - fs::create_directories(dest_file.parent_path()); - - if(params.symlink) { - info(std::string("linking from: ") + p_iter->path().string() + - " to: " + dest_file.string()); - if(!params.dry_run) { - // The target of a symlink must be either absolute (aka complete) or - // relative to the location of the link. Easiest solution is to make - // a complete path. - fs::path target; - if(p_iter->path().is_complete()) - target = p_iter->path(); - else - target = fs::initial_path() / p_iter->path(); - fs::create_symlink(target, dest_file); - } - } - else { - info(std::string("copying from: ") + p_iter->path().string() + - " to: " + dest_file.string()); - if(!params.dry_run) { - // Copy the file and restore its write time (needed for posix) - std::time_t time = fs::last_write_time(*p_iter); - fs::copy_file(*p_iter, dest_file); - fs::last_write_time(dest_file, time); - if(params.verify) { - md5digest src_digest, dst_digest; - bool ok = md5sum(p_iter->path(), src_digest); - if(ok) - ok = md5sum(dest_file, dst_digest); - if(ok) - ok = (memcmp(src_digest,dst_digest, sizeof(md5digest))==0); - if(!ok) { - // Should probably find a more appropriate exception for this - throw std::runtime_error(std::string("File verification failed: '") - + p_iter->path().string() + "' differs from '" + - dest_file.string() + "'"); - } - else { - info(std::string("verification passed")); - } - } - } - } - if(params.move) { - info(std::string("removing: ") + p_iter->path().string()); - if(!params.dry_run) - fs::remove(*p_iter); - } - - if(!g_verbose && (params.ok_count % DOT_EVERY)==0) { - std::cout << "." << std::flush; - g_neednewline = true; - } - ++params.ok_count; - } - catch(fs::filesystem_error& e) { - error(e, std::string("skipping file: " + p_iter->path().string())); - ++params.file_err_count; - } - } - } - } - catch(fs::filesystem_error& e) { - error(e, std::string("skipping directory: " + directory.string())); - ++params.dir_err_count; - } -} - diff --git a/contrib/scripts/clangFormatWholeProject.sh b/contrib/scripts/clangFormatWholeProject.sh new file mode 100755 index 0000000000..0b98437100 --- /dev/null +++ b/contrib/scripts/clangFormatWholeProject.sh @@ -0,0 +1,10 @@ +# This script is making the following assumptions: +# - 1) You are running the script from the project root directory +# - 2) clang-format is in your $PATH + +find src/ -iname *.h* -o -iname *.c* | xargs clang-format -i +find include/ -iname *.h* -o -iname *.c* | xargs clang-format -i +find samples/ -iname *.h* -o -iname *.c* | xargs clang-format -i +find unitTests/ -iname *.h* -o -iname *.c* | xargs clang-format -i +find app/ -iname *.h* -o -iname *.c* | xargs clang-format -i +find fuzz/ -iname *.h* -o -iname *.c* | xargs clang-format -i diff --git a/contrib/vs2019/README.md b/contrib/vs2019/README.md index 0d4827742b..be9e7adae9 100644 --- a/contrib/vs2019/README.md +++ b/contrib/vs2019/README.md @@ -61,7 +61,7 @@ E:\Projects\libexpat. The exiv2 solution will have the exiv2 project exposition) and the expat-static project (from libexpat). The expat-static project is added as a reference to the exiv2 project. This make Visual Studio generate the correct dependency -heirarchy and compile correctly. +hierarchy and compile correctly. # Folder Structure # diff --git a/contrib/vs2019/solution/exv_conf.h b/contrib/vs2019/solution/exv_conf.h index fde14ba1a2..c1683c40d6 100644 --- a/contrib/vs2019/solution/exv_conf.h +++ b/contrib/vs2019/solution/exv_conf.h @@ -3,9 +3,6 @@ #ifndef _EXV_CONF_H_ #define _EXV_CONF_H_ -// Defined if you want to use libssh for SshIO. -#define EXV_USE_SSH - // Define to 1 if you want to use libcurl in httpIO. #define EXV_USE_CURL @@ -15,16 +12,13 @@ // Define if you require PNG support. #define EXIV2_ENABLE_PNG -// Define if you have the `gmtime_r' function. -/* #undef EXV_HAVE_GMTIME_R */ - // Define if you have the header file. /* #undef EXV_HAVE_LIBINTL_H */ // Define if you want translation of program messages to the user's native language #define EXV_ENABLE_NLS -// Define if you want video support. +// Define if you want to support video metadata #define EXV_ENABLE_VIDEO // Define if you have the strerror_r function. diff --git a/doc/ChangeLog b/doc/ChangeLog index 32817b4b0f..72590f8ddc 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,3 +1,147 @@ +Changes from version 0.28.7 to 0.28.8 +------------------------------------- + +Release Notes: + +* https://github.com/Exiv2/exiv2/issues/3467 +* https://github.com/Exiv2/exiv2/milestone/19?closed=1 + +This release fixes three low-severity vulnerabilities: + +* [CVE-2026-25884](https://github.com/Exiv2/exiv2/security/advisories/GHSA-9mxq-4j5g-5wrp) +* [CVE-2026-27596](https://github.com/Exiv2/exiv2/security/advisories/GHSA-3wgv-fg4w-75x7) +* [CVE-2026-27631](https://github.com/Exiv2/exiv2/security/advisories/GHSA-p2pw-7935-c73j) + +Changes from version 0.28.6 to 0.28.7 +------------------------------------- + +Release Notes: + +* https://github.com/Exiv2/exiv2/issues/3379 +* https://github.com/Exiv2/exiv2/milestone/18?closed=1 + +This release reverts an ABI incompatibility that was accidentally introduced in v0.28.6: + +* https://github.com/Exiv2/exiv2/issues/3376 + +Changes from version 0.28.5 to 0.28.6 +------------------------------------- + +Release Notes: + +* https://github.com/Exiv2/exiv2/issues/3323 +* https://github.com/Exiv2/exiv2/milestone/17?closed=1 + +This release fixes two low-severity vulnerabilities: + +* [CVE-2025-54080](https://github.com/Exiv2/exiv2/security/advisories/GHSA-496f-x7cq-cq39) +* [CVE-2025-55304](https://github.com/Exiv2/exiv2/security/advisories/GHSA-m54q-mm9w-fp6g) + +Changes from version 0.28.4 to 0.28.5 +------------------------------------- + +Release Notes: + +* https://github.com/Exiv2/exiv2/issues/3181 +* https://github.com/Exiv2/exiv2/milestone/16?closed=1 + +This release fixes a use-after-free vulnerability in `tiffcomposite_int.cpp`: + +* [CVE-2025-26623](https://github.com/Exiv2/exiv2/security/advisories/GHSA-38h4-fx85-qcx7) + +Versions prior to v0.28.0, such as v0.27.7, are not affected by CVE-2025-26623. + +Changes from version 0.28.3 to 0.28.4 +------------------------------------- + +Release Notes: + +* https://github.com/Exiv2/exiv2/issues/3149 +* https://github.com/Exiv2/exiv2/milestone/15?closed=1 + +Changes from version 0.28.2 to 0.28.3 +------------------------------------- + +Release Notes: + +* https://github.com/Exiv2/exiv2/issues/3008 +* https://github.com/Exiv2/exiv2/milestone/14?closed=1 + +This release also fixes a low-severity security issue in asfvideo.cpp: + +* [CVE-2024-39695](https://github.com/Exiv2/exiv2/security/advisories/GHSA-38rv-8x93-pvrh): out-of-bounds read in AsfVideo::streamProperties. + +This vulnerability is in a new feature (ASF video) that was added in version 0.28.0, so earlier versions of Exiv2 are not affected. + +Changes from version 0.28.1 to 0.28.2 +------------------------------------- + +Release Notes: + +* https://github.com/Exiv2/exiv2/issues/2914 +* https://github.com/Exiv2/exiv2/milestone/13?closed=1 + +This release also fixes two low-severity security issues in quicktimevideo.cpp: + +* [CVE-2024-24826](https://github.com/Exiv2/exiv2/security/advisories/GHSA-g9xm-7538-mq8w): out-of-bounds read in QuickTimeVideo::NikonTagsDecoder. +* [CVE-2024-25112](https://github.com/Exiv2/exiv2/security/advisories/GHSA-crmj-qh74-2r36): denial of service due to unbounded recursion in QuickTimeVideo::multipleEntriesDecoder. + +These vulnerabilities are in a new feature (quicktime video) that was added in version 0.28.0, so earlier versions of Exiv2 are not affected. + +Changes from version 0.28.0 to 0.28.1 +------------------------------------- + +Release Notes: +https://github.com/Exiv2/exiv2/issues/2813 + +This release also fixes [CVE-2023-44398](https://github.com/Exiv2/exiv2/security/advisories/GHSA-hrw9-ggg3-3r4r), an out-of-bounds write in `BmffImage::brotliUncompress`. The vulnerability is in new code that was added in version 0.28.0, so earlier versions of Exiv2 are not affected. + +Changes from version 0.27.6 to 0.28.0 +------------------------------------- + +Release Notes: +https://github.com/Exiv2/exiv2/issues/2406#issuecomment-1529139799 + +Changes from version 0.27.6 to 0.27.7 +------------------------------------- + +Closed: +https://github.com/Exiv2/exiv2/milestone/11?closed=1 + +Open: +https://github.com/Exiv2/exiv2/milestone/11?open=1 + +Release Notes: +https://github.com/Exiv2/exiv2/pull/2567#issuecomment-1546701495 + +Changes from version 0.27.5 to 0.27.6 +------------------------------------- + +Closed: +https://github.com/Exiv2/exiv2/milestone/10?closed=1 + +Open: +https://github.com/Exiv2/exiv2/milestone/10?open=1 + +Release Notes: +https://github.com/Exiv2/exiv2/issues/2406#issuecomment-1383302378 + +Changes from version 0.27.4 to 0.27.5 +------------------------------------- + +Closed: +https://github.com/Exiv2/exiv2/milestone/9?closed=1 + +Open: +https://github.com/Exiv2/exiv2/milestone/9?open=1 + +Release Notes: +https://github.com/Exiv2/exiv2/issues/1018#issuecomment-948573657 + ++++++++++++++++++++++++++++++++++++++ +------------- History --------------- ++++++++++++++++++++++++++++++++++++++ + Changes from version 0.27.3 to 0.27.4 ------------------------------------- @@ -323,7 +467,7 @@ Build: (11) 0001329 Error during cmake with gcc version parsing 0001310 Provide support for msys/2.0 0001299 exiv2-0.26-trunk.tar.gz changed on download server - 0001270 Using libexiv2.a/.lib in multhreaded app segfaults. + 0001270 Using libexiv2.a/.lib in multithreaded app segfaults. 0001237 Report CMake/MinGW issues to Kitware 0001188 Provide build support for C++11 0001174 Visual Studio Support Enhancements @@ -643,9 +787,9 @@ Changes from version 0.25 to 0.26 - 0001145: Respect Sony/Minolta lenses with shared LensID such as Tamron SP AF 17-50mm F2.8 XR Di II LD - 0001144: Sigma 10-20mm f/4-5.6 EX DC is detected as Tamaron (Simon Harhues) - - 0001142: Manual lens does not get recogniced: Beroflex zoom 500mm + - 0001142: Manual lens does not get recognized: Beroflex zoom 500mm (Simon Harhues / Niels Kristian Bech Jensen) - - 0001141: Manual lens does not get recogniced: Pentax macro 100mm + - 0001141: Manual lens does not get recognized: Pentax macro 100mm (Simon Harhues / Niels Kristian Bech Jensen) - 0001118: Add support for ZEISS Loxia 2/50 lens (Eugen Neu) @@ -689,7 +833,7 @@ Changes from version 0.25 to 0.26 - 0000922: Add options -pS and -dI to application exiv2 - 0000855: Segfault when accessing focalLength with 0.23 (Tobias E.) - - 0000756: Access to ICC Profile (TAG: 0x8773) data in Exif as uninterpretted binary + - 0000756: Access to ICC Profile (TAG: 0x8773) data in Exif as uninterpreted binary (Ray NA) - 0000676: Patch for reading ICC color profiles (Andreas Huggel) @@ -783,7 +927,7 @@ Changes from version 0.25 to 0.26 * Testing: (6) - 0001230: Bug Hunt for v0.26 - - 0001207: digiKam maintenance tool to synchronize files metadata and database crash in Exiv2 (re-entrancy issue ?) + - 0001207: digiKam maintenance tool to synchronize files metadata and database crash in Exiv2 (reentrancy issue ?) (Uwe Haider) - 0001057: Implement target/modifier - (stdin/stdout) for exiv2 options -i (insert) and -e (extract) - 0001045: Add COPYRIGHT file to test/data/ @@ -1192,7 +1336,7 @@ Changes from version 0.23 to 0.24 (Reported by S. Verdoold, patch by Pascal de Bruijn) - 0000861: Sigma 18-250mm not properly recognised on Pentax (Reported by S. Verdoold, patch by Pascal de Bruijn) - - 0000862: Video code is failing the test suite (on all plaforms) + - 0000862: Video code is failing the test suite (on all platforms) (Robin Mills, Abhinav Badola) - 0000865: Patches for locale and boost issue (Patches by Mario anyc) - 0000868: Support for two lens for sony mount @@ -1463,7 +1607,7 @@ Changes from version 0.20 to 0.21 (Patch by Matthias Baas) - Added ExifKey::defaultCount() to access the new count reference information. - - Fixed zlib uncompression of large PNG metadata buffers. + - Fixed zlib decompression of large PNG metadata buffers. - Improved determination of MIME type. - Updated Canon makernote. (Greg Mansfield, Axel Waggershauser) - Updated Olympus makernote. (Greg Mansfield) @@ -1713,7 +1857,7 @@ Changes from version 0.18 to 0.18.1 support for multiple TIFF tree structures. * MSVC related - - 0000621: windows librarys are forcing a link to + - 0000621: windows libraries are forcing a link to ..\..\..\zlib-1.2.3\projects\visual6\blah\blah. (Reported by Peter J. Ersts, fix by Robin Mills) @@ -2051,7 +2195,7 @@ Changes from version 0.12 to 0.13 and insert actions. * Exiv2 library - - 0000503: [metadata] Tiff generated by photoshop crashes exiv2 + - 0000503: [metadata] Tiff generated by Photoshop crashes exiv2 (digikam bug 139658). - 0000502: [iptc] New TIFF parser: Decode IPTC from Exif.Image.IPTCNAA. (Reported by Walter Hangartner) @@ -2124,7 +2268,7 @@ Changes from version 0.11 to 0.12 - [exif] Updated Minolta makernote. (Gilles Caulier, Paul Tribick) * MSVC related - - 0000487: Crash in exiv2.exe (built with VC++ 2005 Express) when examing TIFF + - 0000487: Crash in exiv2.exe (built with VC++ 2005 Express) when examining TIFF images (Reported with patch by Dimitri Schoolwerth) diff --git a/doc/cmdxmp.txt b/doc/cmdxmp.txt index 313f8ac542..6355ce1edd 100644 --- a/doc/cmdxmp.txt +++ b/doc/cmdxmp.txt @@ -40,7 +40,7 @@ set Xmp.dc.creator XmpSeq "1) The first creator" set Xmp.dc.creator "2) The second creator" set Xmp.dc.creator "3) And another one" -# A language alternative. The default entry of a langauge alternative +# A language alternative. The default entry of a language alternative # doesn't need a language qualifier. set Xmp.dc.description LangAlt lang=de-DE Hallo, Welt set Xmp.dc.description LangAlt Hello, World @@ -70,7 +70,7 @@ set Xmp.xmpDM.videoFrameSize/stDim:unit inch set Xmp.dc.publisher James Bond set Xmp.dc.publisher[1]/?ns:role secret agent -# Add a qualifer to an array element of Xmp.dc.creator (added above) +# Add a qualifier to an array element of Xmp.dc.creator (added above) set Xmp.dc.creator[2]/?ns:role programmer # Add an array of structures. First set a text property with just the diff --git a/doc/readme-pvs-studio.md b/doc/readme-pvs-studio.md new file mode 100644 index 0000000000..e54afd84b5 --- /dev/null +++ b/doc/readme-pvs-studio.md @@ -0,0 +1,31 @@ +# How to integrate PVS-Studio + +We obtained a free license of PVS-Studio due to the open source nature of Exiv2. + +## CMake integration + +To check a project configured with CMake, such as Exiv2, we need to generate the JSON compilation database. + +```bash +# Under an already configured "buildXXX" directory +cd buildXXX +cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ... + +# Once the CMake configuration is done, we should obtain a output file 'compile_commands.json' +# NOTE: This option is enabled by default inside our CMake configuration (see cmake/mainSetup.cmake) +``` + +The analysis starts by running the following commands: + +```bash +export PVS_LICENSE=~/.config/PVS-Studio/PVS-Studio.lic +pvs-studio-analyzer analyze -l $PVS_LICENSE -o pvsStudio.log -j8 +plog-converter -a GA:1,2 -t tasklist pvsStudio.log -o pvsStudio.tasks +plog-converter -a GA:1,2 -t fullhtml pvsStudio.log -o pvsReportHtml +plog-converter -a GA:1,2 -d V1042 -t fullhtml pvsStudio.log -o pvsReportHtml +``` + +## CI + +The PVS username & key are configured as secrets in the security settings of the project. Then we make use of such +secrets in the Github workflow named `special_pvsStudio`. diff --git a/doc/templates/Makefile b/doc/templates/Makefile index cfdcd849e3..67101d00cb 100644 --- a/doc/templates/Makefile +++ b/doc/templates/Makefile @@ -50,9 +50,20 @@ TABLES = Exif \ CanonSi \ CanonCf \ CanonPi \ + CanonTi \ CanonFi \ CanonPa \ CanonPr \ + CanonMe \ + CanonHdr \ + CanonVigCor2 \ + CanonLiOp \ + CanonAfMiAdj \ + CanonLe \ + CanonAm \ + CanonFil \ + CanonAfC \ + CanonRawB \ Casio \ Casio2 \ Fujifilm \ @@ -69,12 +80,15 @@ TABLES = Exif \ NikonIi \ NikonAf \ NikonAf2 \ + NikonAf22 \ NikonAFT \ NikonFi \ NikonMe \ NikonFl1 \ NikonFl2 \ NikonFl3 \ + NikonFl6 \ + NikonFl7 \ NikonSiD80 \ NikonSiD40 \ NikonSiD300a \ @@ -89,6 +103,7 @@ TABLES = Exif \ NikonLd1 \ NikonLd2 \ NikonLd3 \ + NikonLd4 \ Olympus \ OlympusCs \ OlympusEq \ @@ -108,10 +123,15 @@ TABLES = Exif \ SonyMinolta \ Sony1Cs \ Sony1Cs2 \ + Sony2010e \ Sony1MltCs7D \ Sony1MltCsOld \ Sony1MltCsA100 \ - Sony2Fp + Sony2Fp \ + SonyMisc1 \ + SonyMisc2b \ + SonyMisc3c \ + SonySInfo1 SCHEMA = xmp_dc \ xmp_dwc \ @@ -147,7 +167,7 @@ SCHEMA = xmp_dc \ xmp_mwg-rs \ xmp_mwg-kw -TAGLIST = ${EXIV2HOME}/build/bin/taglist +TAGLIST = ${EXIV2_BINDIR}/taglist # ********************************************************************** # ====================================================================== diff --git a/doc/templates/gen.py b/doc/templates/gen.py index afc390d18a..17bb6a7323 100755 --- a/doc/templates/gen.py +++ b/doc/templates/gen.py @@ -1,57 +1,62 @@ #! /usr/bin/env python +import sys +import os +import re +import time + # ---------------------------------------------------------------------- # Settings vardir = "." date_format = "%d-%b-%Y" + # ---------------------------------------------------------------------- # functions def usage(): - print("""Usage: gen.py file.in [...] - Substitute placeholders in input files with content - """) + print("""Usage: gen.py file.in [...] + Substitute placeholders in input files with content + """) + def gen_html(file): - """Replace variables in the file with their content""" - text = open(file).read() - for var in vars: - vartext = open(vardir + "/" + var).read() - text = text.replace(var, vartext) - text = last_modified(text) - return text + """Replace variables in the file with their content""" + text = open(file, encoding=None).read() + for var in vars: + vartext = open(f"{vardir}/{var}", encoding=None).read() + text = text.replace(var, vartext) + text = last_modified(text) + return text + def last_modified(text): - """Substitute variable __last_modified__ with the current date""" - date = time.strftime(date_format, time.localtime()) - text = text.replace("__last_modified__", date) - return text + """Substitute variable __last_modified__ with the current date""" + date = time.strftime(date_format, time.localtime()) + text = text.replace("__last_modified__", date) + return text + # ---------------------------------------------------------------------- # main -import sys -import os -import re -import time - # Check command line arguments if len(sys.argv) == 1: - usage() - sys.exit() + usage() + sys.exit() # The input files from the command line input = sys.argv[1:] -# Get a list of all variables (files in the form __*__) from vardir +# Get a list of all variables (files in the form __*__) from vardir vars = os.listdir(vardir) -for i in range(len(vars)-1, -1, -1): - if re.match("^__.*__$", vars[i]): continue - del vars[i] +for i in range(len(vars) - 1, -1, -1): + if re.match("^__.*__$", vars[i]): + continue + del vars[i] vars.sort() # Substitute variables in all input files -print("Substituting variables {0}".format(vars)) +print(f"Substituting variables {vars}") for file in input: - print("Processing {0}...".format(file)) - text = gen_html(file) - file = file.replace(".in", "") - open(file, 'w').write(text) + print(f"Processing {file}...") + text = gen_html(file) + file = file.replace(".in", "") + open(file, "w", encoding=None).write(text) diff --git a/doc/templates/iptc.py b/doc/templates/iptc.py index 09609d88b3..6b79a47111 100755 --- a/doc/templates/iptc.py +++ b/doc/templates/iptc.py @@ -20,18 +20,18 @@ data = sys.stdin.readlines() for line in csv.reader(data,quotechar='"',skipinitialspace=True): row=row+1 - print(" " % row) - print(" " + line[ 0] + "") - print(" " + line[ 1] + "") - print(" " + line[ 2] + "") - print(" " + line[ 3] + "") - print(" " + line[ 4] + "") - print(" " + line[ 5] + "") - print(" " + line[ 6] + "") - print(" " + line[ 7] + "") - print(" " + line[ 8] + "") - print(" " + line[ 9] + "") - print(" " + line[10] + "") + print(f" ") + print(f" {line[0]}") + print(f" {line[1]}") + print(f" {line[2]}") + print(f" {line[3]}") + print(f" {line[4]}") + print(f" {line[5]}") + print(f" {line[6]}") + print(f" {line[7]}") + print(f" {line[8]}") + print(f" {line[9]}") + print(f" {line[10]}") print(" ") print("") diff --git a/doc/templates/tags-canon.html.in b/doc/templates/tags-canon.html.in index 629b5a1d49..92530866a0 100644 --- a/doc/templates/tags-canon.html.in +++ b/doc/templates/tags-canon.html.in @@ -39,16 +39,71 @@ __CanonCf__ __CanonPi__
-

Canon File Info Tags

+

Canon Filter Info Tags

Click on a column header to sort the table.

__CanonFi__
+

Canon High Dynamic Range Tags

+

Click on a column header to sort the table.

+__CanonHdr__ +
+ +

Canon Vignetting Correction 2 Tags

+

Click on a column header to sort the table.

+__CanonVigCor2__ +
+ +

Canon Lighting Option Tags

+

Click on a column header to sort the table.

+__CanonLiOp__ +
+ +

Canon AF Micro Adjust Tags

+

Click on a column header to sort the table.

+__CanonAfMiAdj__ +
+ +

Canon Lens Info Tags

+

Click on a column header to sort the table.

+__CanonLe__ +
+ +

Canon Ambience Selection Tags

+

Click on a column header to sort the table.

+__CanonAm__ +
+ +

Canon Filter Info Tags

+

Click on a column header to sort the table.

+__CanonFil__ +
+

Canon Processing Info Tags

Click on a column header to sort the table.

__CanonPr__
+

Canon Multi-Exposure Tags

+

Click on a column header to sort the table.

+__CanonMe__ +
+ +

Canon AF Config Tags

+

Click on a column header to sort the table.

+__CanonAfC__ +
+ +

Canon Raw Burst Info Tags

+

Click on a column header to sort the table.

+__CanonRawB__ +
+ +

Canon TimeInfo Tags

+

Click on a column header to sort the table.

+__CanonTi__ +
+
diff --git a/doc/templates/tags-nikon.html.in b/doc/templates/tags-nikon.html.in index f7559e45f6..0b5d71390d 100644 --- a/doc/templates/tags-nikon.html.in +++ b/doc/templates/tags-nikon.html.in @@ -61,6 +61,11 @@ __NikonAf__ __NikonAf2__
+

Nikon Auto Focus 2 V2 Tags

+

Click on a column header to sort the table.

+__NikonAf22__ +
+

Nikon AF Fine Tune Tags

Click on a column header to sort the table.

__NikonAFT__ @@ -91,6 +96,16 @@ __NikonFl2__ __NikonFl3__
+

Nikon Flash Info 6 Tags

+

Click on a column header to sort the table.

+__NikonFl6__ +
+ +

Nikon Flash Info 7 Tags

+

Click on a column header to sort the table.

+__NikonFl7__ +
+

Nikon Shot Info D80 Tags

Click on a column header to sort the table.

__NikonSiD80__ @@ -161,6 +176,15 @@ __NikonLd2__ __NikonLd3__
+

Nikon Lens Data 4 Tags

+

Click on a column header to sort the table.

+__NikonLd4__ +
+ +

Nikon Preview Tags

+

These are the same as Exif.Image.* but are accessed as Exif.NikonPv.*

+
+
diff --git a/doc/templates/tags-samsung.html.in b/doc/templates/tags-samsung.html.in index 11a86f1daf..2cb698345c 100644 --- a/doc/templates/tags-samsung.html.in +++ b/doc/templates/tags-samsung.html.in @@ -19,6 +19,9 @@ __Samsung2__ __SamsungPictureWizard__
+

Samsung Preview Tags

+

These are the same as Exif.Image.* but are accessed as Exif.SamsungPreview.*

+
diff --git a/doc/templates/tags-sony.html.in b/doc/templates/tags-sony.html.in index 4af010f76c..521a5d6c59 100644 --- a/doc/templates/tags-sony.html.in +++ b/doc/templates/tags-sony.html.in @@ -8,7 +8,7 @@ __index1__ __index2__

Sony MakerNote Tags defined in Exiv2

-

Tags found in the MakerNote of images taken with Sony cameras.

+

Tags found in the MakerNote of images taken with Sony cameras. Sony1 and Sony2 Groups share the same tagnames

Click on a column header to sort the table.

__Sony1__ @@ -31,6 +31,11 @@ __Sony1Cs__ __Sony1Cs2__
+

Sony 2010e Camera Settings Tags

+

Click on a column header to sort the table.

+__Sony2010e__ +
+

Sony Minolta Camera Settings Tags

These are the same tags as the Minolta Camera Settings in the Minolta MakerNote. In Sony images the group name is Sony1MltCsOld or Sony1MltCsNew.

@@ -50,6 +55,31 @@ __Sony1MltCs7D__ __Sony1MltCsA100__
+

Sony Focus Position 2 Tags

+

Click on a column header to sort the table.

+__Sony2Fp__ +
+ +

Sony Miscellaneous 1 Tags

+

Click on a column header to sort the table.

+__SonyMisc1__ +
+ +

Sony Miscellaneous 2b Tags

+

Click on a column header to sort the table.

+__SonyMisc2b__ +
+ +

Sony Miscellaneous 3c Tags

+

Click on a column header to sort the table.

+__SonyMisc3c__ +
+ +

Sony Shot Info 1 Tags

+

Click on a column header to sort the table.

+__SonySInfo1__ +
+
diff --git a/doc/templates/tags-xmp-GPano.html.in b/doc/templates/tags-xmp-GPano.html.in index 6ecc1a2970..b37ee073b0 100644 --- a/doc/templates/tags-xmp-GPano.html.in +++ b/doc/templates/tags-xmp-GPano.html.in @@ -26,7 +26,7 @@ __index2__ -Reference: Photo Sphere XMP Metadata

+Reference: Photo Sphere XMP Metadata

Click on a column header to sort the table.

diff --git a/doc/templates/tags-xmp-iptc.html.in b/doc/templates/tags-xmp-iptc.html.in index 1eda891eb2..bbb506526a 100644 --- a/doc/templates/tags-xmp-iptc.html.in +++ b/doc/templates/tags-xmp-iptc.html.in @@ -10,13 +10,11 @@ __index2__

IPTC Core schema

This schema specifies the IPTC Core XMP properties.

    -
  • Exiv2 keys are Xmp.iptc.<Property>
  • -
  • Creator Contact Info sub-keys are nested: Xmp.Iptc4xmpCore.CreatorContactInfo/Iptc4xmpCore:<Property>
  • +
  • The Exiv2 keys are either Xmp.iptc.<Property> or Xmp.Iptc4xmpCore.<Property>
  • The schema namespace URI is http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/
  • The preferred schema namespace prefix is Iptc4xmpCore
-Reference: "IPTC Core" Specification Version 1.2

-

Note: Exiv2 uses iptc (rather than Iptc4xmpCore) as the group name for keys of IPTC Core schema properties.

+Reference: IPTC Core Metadata Schema 1.3 specification

Click on a column header to sort the table.

diff --git a/doc/templates/tags-xmp-iptcExt.html.in b/doc/templates/tags-xmp-iptcExt.html.in index 2d8cfb6808..023c68ea50 100644 --- a/doc/templates/tags-xmp-iptcExt.html.in +++ b/doc/templates/tags-xmp-iptcExt.html.in @@ -13,12 +13,11 @@ __index2__ of more granular properties and further specialized rights related properties from the PLUS metadata schema.
    -
  • Exiv2 keys are Xmp.iptcExt.<Property>
  • +
  • The Exiv2 keys are either Xmp.iptcExt.<Property> or Xmp.Iptc4xmpExt.<Property>
  • The schema namespace URI is http://iptc.org/std/Iptc4xmpExt/2008-02-29/
  • The preferred schema namespace prefix is Iptc4xmpExt
-Reference: IPTC Extension Specification Version 1.2 standard

-

Note: Exiv2 uses iptcExt (rather than Iptc4xmpExt) as the group name for keys of IPTC Extension schema properties.

+Reference: IPTC Extension Metadata Schema 1.6 specification

Click on a column header to sort the table.

diff --git a/doc/templates/tags.py b/doc/templates/tags.py index 60d99c3ab3..a39dbf8b07 100755 --- a/doc/templates/tags.py +++ b/doc/templates/tags.py @@ -20,14 +20,14 @@ data = sys.stdin.readlines() for line in csv.reader(data,quotechar='"',skipinitialspace=True): row=row+1 - print(" " % row) - print(" " + line[0] + "") - print(" " + line[1] + "") - print(" " + line[2] + "") - print(" " + line[3] + "") - print(" " + line[4] + "") - print(" " + line[5] + "") - print(" " + line[6] + "") + print(f" ") + print(f" {line[0]}") + print(f" {line[1]}") + print(f" {line[2]}") + print(f" {line[3]}") + print(f" {line[4]}") + print(f" {line[5]}") + print(f" {line[6]}") print(" ") print("") diff --git a/doc/templates/xmp.py b/doc/templates/xmp.py index df8b14d6c9..792cf48c53 100755 --- a/doc/templates/xmp.py +++ b/doc/templates/xmp.py @@ -20,13 +20,13 @@ print(data) for line in csv.reader(data,quotechar='"',skipinitialspace=True): row=row+1 - print(" " % row) - print(" " + line[0] + "") - print(" " + line[1] + "") - print(" " + line[2] + "") - print(" " + line[3] + "") - print(" " + line[4] + "") - print(" " + line[5] + "") + print(f" ") + print(f" {line[0]}") + print(f" {line[1]}") + print(f" {line[2]}") + print(f" {line[3]}") + print(f" {line[4]}") + print(f" {line[5]}") print(" ") print("") diff --git a/em.txt b/em.txt new file mode 100644 index 0000000000..5956c1506c --- /dev/null +++ b/em.txt @@ -0,0 +1,11 @@ +[binaries] +c = 'emcc' +cpp = 'em++' +ar = 'emar' +nm = 'emnm' + +[host_machine] +system = 'emscripten' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' diff --git a/exiv2.md b/exiv2.md new file mode 100644 index 0000000000..149f53b809 --- /dev/null +++ b/exiv2.md @@ -0,0 +1,1872 @@ +
+ +# EXIV2(1) + +
+ +# NAME +exiv2 - Image metadata manipulation tool + +
+ +# 1 SYNOPSIS +**exiv2** [ **option** [ *arg* ] ]+ [ *action* ] *file* ... + +
+ +# 2 DESCRIPTION +**exiv2** is a program to read and write image metadata, including +Exif, IPTC, XMP, image comments, ICC Profile, thumbnails, image +previews and many vendor makernote tags. The program optionally +converts between Exif, IPTC and XMP tags, as recommended by their +respective standards/specifications and the Metadata Working Group +guidelines. + +
+ +### TABLE OF CONTENTS +1. [SYNOPSIS](#synopsis) +2. [DESCRIPTION](#description) +3. [FILE TYPES](#file_types) +4. [ACTIONS](#actions) +5. [COMMAND SUMMARY](#cmd_summary) +6. [OPTIONS](#options) +7. [EXIV2 GROUPS, TYPES AND VALUES](#groups_types_values) + 1. [Exiv2 key syntax](#exiv2_key_syntax) + 2. [Exiv2 tags](#exiv2_tags) + 3. [Exif/IPTC/XMP types](#exiv2_types) + 4. [Multiple elements](#multi_elements) + 5. [Duplicate tags](#multi_tags) + 6. [Date/Time formats](#date_time_fmts) + 7. [Exif 'Comment' values](#exif_comment_values) + 8. [XMP namespaces](#xmp_namespaces) + 9. [XMP LangAlt values](#langalt_values) + 10. [XMP structs](#xmp_structs) +8. [PREVIEW IMAGES AND THUMBNAILS](#preview_images) +9. [ICC PROFILES](#icc_profiles) +10. [IMAGE COMMENTS](#image_comments) +11. ['MODIFY' COMMANDS](#modify_cmds) + 1. [Quotations with 'modify' commands](#quotes_modify) + 2. ['Modify' command format](#mod_cmd_format) + 3. [Modifying a value](#mod_value) + 4. ['Modify' examples](#mod_examples) + 5. ['Modify' command file](#mod_cmd_file) +12. [CONFIGURATION FILE](#config_file) +13. [EXAMPLES](#examples) +14. [RETURN VALUE](#return_value) +15. [ENVIRONMENT](#environment) +16. [NOTES](#notes) +17. [BUGS](#bugs) +18. [COPYRIGHT](#copyright) +19. [AUTHORS](#authors) +20. [SEE ALSO](#see_also) + +
+ +# 3 FILE TYPES + +The following image formats and metadata categories are supported:
+ +Type | Exif | IPTC | XMP | Image Comments | ICC Profile | Thumbnail +|:---|:---- |:---- |:---- |:---- |:---- |:---- +ARW | Read/Write | Read/Write | Read/Write | - | Read/Write | Read/Write +AVIF | Read | Read | Read | - | - | Read +BMP | - | - | - | - | - | - +CR2 | Read/Write | Read/Write | Read/Write | - | Read/Write | Read/Write +CR3 | Read | Read | Read | - | - | Read +CRW | Read/Write | - | - | Read/Write | - | Read/Write +DCP | Read/Write | - | - | - | - | - +DNG | Read/Write | Read/Write | Read/Write | - | Read/Write | Read/Write +EPS | - | - | Read/Write | | - | - +EXV | Read/Write | Read/Write | Read/Write | Read/Write | Read/Write | Read/Write +GIF | - | - | - | - | - | - +HEIC | Read | Read | Read | - | - | Read +HEIF | Read | Read | Read | - | - | Read +JP2 | Read/Write | Read/Write | Read/Write | - | Read/Write | Read/Write +JPEG | Read/Write | Read/Write | Read/Write | Read/Write | Read/Write | Read/Write +JXL | Read | Read | Read | - | - | Read +MRW | Read | Read | Read | - | - | Read +NEF | Read/Write | Read/Write | Read/Write | - | Read/Write | Read/Write +ORF | Read/Write | Read/Write | Read/Write | - | - | Read/Write +PEF | Read/Write | Read/Write | Read/Write | - | Read/Write | Read/Write +PGF | Read/Write | Read/Write | Read/Write | Read/Write | Read/Write | - +PNG | - | Read/Write | Read/Write | - | Read/Write | Read/Write +PSD | Read/Write | Read/Write | Read/Write | - | - | Read/Write +RAF | Read | Read | Read | - | - | Read +RW2 | Read | Read | Read | - | - | Read +SR2 | Read | Read | Read | - | - | Read +SRW | Read/Write | Read/Write | Read/Write | - | - | Read/Write +TGA | - | - | - | - | - | - +TIFF | Read/Write | Read/Write | Read/Write | - | Read/Write | Read/Write +WEBP | Read/Write | - | Read/Write | - | Read/Write | Read/Write +XMP | - |- | Read/Write | - | - | - + +- Support for GIF, TGA and BMP images is minimal: the image format is +recognized, a MIME type assigned to it and the height and width of the +image are determined. + +- Reading other TIFF-like RAW image formats, which are not listed in +the table, may also work. + +- Some image formats allow an extra internal type of metadata. Only +partial support exists for the RAF format. + +- Support for BMFF types such as AVIF, CR3, HEIF and HEIC is a build +option. To check if this is enabled, use `exiv2 --version --verbose --grep bmff` +and see if `enable_bmff=1`. + +- Naked codestream JXL files do not contain Exif, IPTC or XMP metadata. + +- Support of video files is limited. Currently **exiv2** only has some + rudimentary support to read metadata from quicktime, matroska and riff based video files (e.g. + .MOV/.MP4, .MKV, .AVI, .WAV, .ASF). + + +[TOC](#TOC) + +
+ +# 4 ACTIONS +The *action* argument is only required if it is not clear from the +*options* which action is implied. + +
+ +### pr | print +Print image metadata, the default is [--print s](#print_mod). This is also +the default action (i.e., `exiv2 image.jpg`). + +
+ +### ex | extract +Extract metadata to "raw" metadata (\*.exv), XMP sidecar (\*.xmp), +preview image, thumbnail or ICC profile file. The default is [--extract XXei](#extract_tgt3). +Use [--location dir](#location_dir) to direct the output to a different +directory. Modification commands can be applied on-the-fly. + +
+ +### in | insert +Insert metadata from corresponding "raw" metadata (\*.exv), XMP sidecar (\*.xmp), +thumbnail or ICC profile files, the default is [--insert XXeix](#insert_tgt2). +Use [--location dir](#location_dir) to direct the input from a different +directory and [--suffix suf](#suffix_suf) for the output extension. Since +files of any supported format can be used as input files, this command +can be used to copy the metadata between files of different formats. + +
+ +### rm | delete +Delete image metadata from the files, the default is [--delete a](#delete_tgt1). + +
+ +### ad | adjust +Adjust Exif timestamps by the given time. Requires at least one of the +options [--adjust time](#adjust_time), [--years +-n](#years_n), +[--months +-n](#months_n) or [--days +-n](#days_n). See [TZ environment variable](#TZ). + +
+ +### mo | modify +Apply commands to modify the Exif, IPTC and XMP metadata of image files. +Requires option [--comment txt](#comment_txt), [--modify cmdfile](#modify_cmdfile) +or [--Modify cmd](#Modify_cmd). + +
+ +### mv | rename +Rename files and/or set file timestamps according to the Exif create +timestamp. The default filename format is in [--rename fmt](#rename_fmt). +Uses the value of [Exif.Photo.DateTimeOriginal](https://www.exiv2.org/tags.html) +or, [Exif.Image.DateTime](https://www.exiv2.org/tags.html) to determine +the timestamp. The filename format can be set with [--rename fmt](#rename_fmt), +timestamp options are [--timestamp](#timestamp) and [--Timestamp](#Timestamp). +See [TZ environment variable](#TZ). + +
+ +### fi | fixiso +Copy the ISO setting from one of the proprietary Nikon or Canon +makernote ISO tags to the regular Exif ISO tag, +[Exif.Photo.ISOSpeedRatings](https://www.exiv2.org/tags.html). Does not +overwrite an existing Exif ISO tag. + +
+ +### fc | fixcom +Fix the character encoding of Exif Unicode user comments. Decodes the +comment using the auto-detected or specified character encoding and +writes it back in UCS-2. Use option [--encode enc](#encode_enc) to +specify the current encoding of the comment if necessary. + +[TOC](#TOC) + +
+ +# 5 COMMAND SUMMARY + +**exiv2** [ **option** [ *arg* ] ]+ [ *action* ] *file* ...
+ +Where *file* is one or more files containing image metadata. These can +optionally be specified using a URL (http, https, ftp, sftp, data +and file supported) or a wildcard pattern (e.g., *image1.tiff image2.jpg*, +*https://www.exiv2.org/Stonehenge.jpg* or *\*.jpg*) + +| **Option** *arg* | **Long option** *arg* | Description | +|:------ |:---- |:---- | +| **-a** *time* | **--adjust** *time* | Automatically modify metadata time stamps. For the [adjust](#ad_adjust) action [[...]](#adjust_time) | +| **-b** | **--binary** | Obsolete and should not be used. Reserved for use with the test suite | +| **-c** *txt* | **--comment** *txt* | JPEG comment string to set in the image. For the [modify](#mo_modify) action [[...]](#comment_txt) | +| **-d** *tgt1* | **--delete** *tgt1* | Delete target(s) for the [delete](#rm_delete) action [[...]](#delete_tgt1) | +| **-D** *+-n* | **--days** *+-n* | Automated adjustment of the days in metadata dates [[...]](#days_n) | +| **-e** *tgt3* | **--extract** *tgt3* | Extract target(s) for the [extract](#ex_extract) action [[...]](#extract_tgt3) | +| **-f** | **--force** | Do not prompt before overwriting existing files. For the [rename](#mv_rename) and [extract](#ex_extract) actions [[...]](#force_Force) | +| **-F** | **--Force** | Do not prompt before renaming files. For the [rename](#mv_rename) and [extract](#ex_extract) actions [[...]](#force_Force) | +| **-g** *str* | **--grep** *str* | Only output where *str* matches in output text [[...]](#grep_str) | +| **-h** | **--help** | Display help and exit [[...]](#help) | +| **-i** *tgt2* | **--insert** *tgt2* | Insert target(s) for the [insert](#in_insert) action [[...]](#insert_tgt2) | +| **-k** | **--keep** | Preserve file timestamps when updating files [[...]](#keep) | +| **-K** *key* | **--key** *key* | Report a key. Similar to [--grep str](#grep_str), however *key* must match exactly [[...]](#key_key) | +| **-l** *dir* | **--location** *dir* | Location (directory) for files to be inserted or extracted [[...]](#location_dir) | +| **-m** *cmdfile* | **--modify** *cmdfile* | Read commands from a file. For the [modify](#mo_modify) action [[...]](#modify_cmdfile) | +| **-M** *cmd* | **--Modify** *cmd* | Modify the metadata with the command. For the [modify](#mo_modify) action [[...]](#Modify_cmd) | +| **-n** *enc* | **--encode** *enc* | Charset to decode Exif Unicode user comments [[...]](#encode_enc) | +| **-O** *+-n* | **--months** *+-n* | Automated adjustment of the months in metadata dates [[...]](#months_n) | +| **-p** *mod* | **--print** *mod* | Print report (common reports) [[...]](#print_mod) | +| **-P** *flg* | **--Print** *flg* | Print report (fine grained control) [[...]](#Print_flgs) | +| **-q** | **--quiet** | Silence warnings and error messages [[...]](#quiet) | +| **-Q** *lvl* | **--log** *lvl* | Set the log-level [[...]](#log_lvl) | +| **-r** *fmt* | **--rename** *fmt* | Filename format for the [rename](#mv_rename) action [[...]](#rename_fmt) | +| **-S** *suf* | **--suffix** *suf* | Use suffix for source files when using the [insert](#in_insert) action [[...]](#suffix_suf) | +| **-t** | **--timestamp** | Set the file timestamp from Exif metadata. For the [rename](#mv_rename) action [[...]](#timestamp) | +| **-T** | **--Timestamp** | Only set the file timestamp from Exif metadata. For the [rename](#mv_rename) action [[...]](#Timestamp) | +| **-u** | **--unknown** | Show unknown tags [[...]](#unknown) | +| **-v** | **--verbose** | Verbose [[...]](#verbose) | +| **-V** | **--version** | Show the program version and exit [[...]](#version) | +| **-Y** *+-n* | **--years** *+-n* | Automated adjustment of the years in metadata dates [[...]](#years_n) | + +
+ +The arguments for those options are: + +| *arg* | Description | +|:------ |:---- | +| *action* | pr \| ex \| in \| rm \| ad \| mo \| mv \| fi \| fc
(print, extract, insert, delete, adjust, modify, rename, fixiso, fixcom) | +| *cmd* | (**set** \| **add**) *key* [ [*type*] *value* ] \| **del** *key* [*type*] \| **reg** *prefix* *namespace*
(see ['Modify' command format](#mod_cmd_format)) | +| *enc* | Values defined in [iconv_open(3)](https://linux.die.net/man/3/iconv_open) (e.g., UTF-8) | +| *flg* | E \| I \| X \| x \| g \| k \| l \| n \| y \| c \| s \| v \| t \| h
(Exif, IPTC, XMP, num, grp, key, label, name, type, count, size, vanilla, translated, hex) | +| *fmt* | Default format: %Y%m%d_%H%M%S | +| *key* | See [Exiv2 key syntax](#exiv2_key_syntax) | +| *lvl* | d \| i \| w \| e \| m
(debug, info, warning, error, mute) | +| *mod* | s \| a \| e \| t \| v \| h \| i \| x \| c \| p \| C \| R \| S \| X
(summary, all, Exif, translated, vanilla, hex, IPTC, XMP, comment, preview, ICC Profile, Recursive Structure, Simple Structure, raw XMP) | +| *suf* | '.' then the file's extension (e.g., '.txt') | +| *time* | [+\|-]HH[:MM[:SS]]
(Default is **+** when **+**/**-** are missing) | +| *tgt1* | a \| c \| e \| i \| I \| t \| x \| C \| -
(all, comment, Exif, IPTC, IPTC all, thumbnail, XMP, ICC Profile, stdin/out) | +| *tgt2* | a \| c \| e \| i \| t \| x \| C \| X \| XX \| -
(all, comment, Exif, IPTC, thumbnail, XMP, ICC Profile, SideCar, Raw metadata, stdin/out) | +| *tgt3* | a \| e \| i \| p \| t \| x \| C \| X \| XX \| -
(all, Exif, IPTC, preview, thumbnail, XMP, ICC Profile, SideCar, Raw metadata, stdin/out) | +| *type* | An Exif, IPTC or XMP tag type (e.g., xmpText). See [Exif/IPTC/XMP types](#exiv2_types) | +| *+-n* | The amount to change in the date (e.g., -3). Default is **+** when **+**/**-** are missing | + +[TOC](#TOC) + +
+ +# 6 OPTIONS + +
+ +### **-h**, **--help** +Display help and exit. + +
+ +### **-V**, **--version** +Show the program version and exit. + +When **--version** is combined with [--verbose](#verbose), build +information is printed to standard output along with a list of shared +libraries which have been loaded into memory. Verbose version is +supported on Windows (MSVC, Cygwin and MinGW builds), macOS and Linux +and is provided for test and debugging. The library name and version +number are always printed, even if output is filtered with +[--grep str](#grep_str). + +
+ +### **-v**, **--verbose** +Be verbose during the program run. + +
+ +### **-q**, **--quiet** +Silence warnings and error messages during the program run. Note that +options **--quiet** and [--verbose](#verbose) can be used at the same +time. + +
+ +### **-Q** *lvl*, **--log** *lvl* +Set the log-level to 'd'(ebug), 'i'(nfo), 'w'(arning), 'e'(rror) +or 'm'(ute), with the *lvl* chosen including those below it +('d' \<- 'i' \<- 'w' \<- 'e'). The default log-level is 'w'. **--log** *m* +is equivalent to [--quiet](#quiet). All log messages are written to +standard error. + +
+ +### **-u**, **--unknown** +Show unknown tags. Default is to suppress tags which don't have a name +(e.g., Exif.SonyMisc3c.0x022b). + +
+ +### **-g** *str*, **--grep** *str* +When printing tags, display only those where *str* is found in +the key (see [Exiv2 key syntax](#exiv2_key_syntax)). When +**--grep** *str* is used with [--verbose](#verbose) +[--version](#version), lines are included where *str* matches in the +variable or value. + +Multiple **--grep** *str* options can be used to output additional data: + +``` +$ exiv2 --verbose --version --grep webready --grep time +exiv2 1.0.0.9 +time=11:01:53 +enable_webready=1 +xmlns=mediapro:http://ns.iview-multimedia.com/mediapro/1.0/ +``` + +When the [--print mod](#print_mod) and [--Print flgs](#Print_flgs) +options are not specified, the default is [--print a](#print_mod): + +``` +$ curl --silent -O https://www.exiv2.org/Stonehenge.jpg +$ exiv2 --grep Date Stonehenge.jpg +Exif.Image.DateTime Ascii 20 2015:07:16 20:25:28 +Exif.Photo.DateTimeOriginal Ascii 20 2015:07:16 15:38:54 +Exif.Photo.DateTimeDigitized Ascii 20 2015:07:16 15:38:54 +Exif.NikonWt.DateDisplayFormat Byte 1 Y/M/D +Exif.GPSInfo.GPSDateStamp Ascii 11 2015:07:16 +Xmp.xmp.ModifyDate XmpText 25 2015-07-16T20:25:28+01:00 +``` + +You may use [--print mod](#print_mod) or [--Print flgs](#Print_flgs) to +further filter output ([--print s](#print_mod) is ignored): + +``` +$ exiv2 --print x --grep Date Stonehenge.jpg +Xmp.xmp.ModifyDate XmpText 25 2015-07-16T20:25:28+01:00 +``` + +*str* can contain an optional */i* modifier at the end, to indicate case +insensitivity: + +``` +$ exiv2 --print px --grep date/i Stonehenge.jpg +Xmp.xmp.ModifyDate XmpText 25 2015-07-16T20:25:28+01:00 +``` + +
+ +### **-K** *key*, **--key** *key* +Only reports tags for a given *key*, which must match the key exactly +(See [Exiv2 key syntax](#exiv2_key_syntax)). + +Multiple **--key** *key* options can be used to report more than a +single key: + +``` +$ curl --silent -O https://www.exiv2.org/Stonehenge.jpg +$ exiv2 --key Exif.Photo.DateTimeDigitized --key Exif.Photo.DateTimeOriginal Stonehenge.jpg +Exif.Photo.DateTimeOriginal Ascii 20 2015:07:16 15:38:54 +Exif.Photo.DateTimeDigitized Ascii 20 2015:07:16 15:38:54 +``` + +
+ +### **-n** *enc*, **--encode** *enc* +Charset to use when decoding Exif Unicode user comments, where *enc* is +a name understood by [iconv_open(3)](https://linux.die.net/man/3/iconv_open) +(e.g., 'UTF-8'). See [Exif 'Comment' values](#exif_comment_values). + +
+ +### **-k**, **--keep** +Preserve file timestamps when updating files. Can be used with +all options which update files and is ignored by read-only +options. + +
+ +### **-t**, **--timestamp** +Set the file timestamp according to the Exif create timestamp in +addition to renaming the file (overrides [--keep](#keep)). This option is +only used with the [rename](#mv_rename) action. See [TZ](#TZ) environment +variable. + +
+ +### **-T**, **--Timestamp** +Only set the file timestamp according to the Exif create timestamp, do +not rename the file (overrides [--keep](#keep)). This option is only used +with the [rename](#mv_rename) action. See [TZ environment variable](#TZ). + +
+ +### **-f**, **--force** or **-F**, **--Force** +These options are used by the [rename](#mv_rename) and +[extract](#ex_extract) actions to determine the file overwrite policy. +The options override the default behavior, which is to prompt the user if +the filename already exists. These options are usually combined with +[--verbose](#verbose), to provide additional status output. + +The [rename](#mv_rename) action will overwrite files when **--force** is +used. Instead, if **--Force** is used and the file already exists, the +new filename is appended with '_1' ('_2', ...) to prevent data loss. + +The [extract](#ex_extract) action will overwrite files when either +**--force** or **--Force** is used. + +For example, renaming a file using **--Force**, where the same filename +has already been renamed: + +``` +$ curl --silent -O https://www.exiv2.org/Stonehenge.jpg +$ exiv2 --verbose --Force rename Stonehenge.jpg +File 1/1: Stonehenge.jpg +Renaming file to ./20150716_153854.jpg + +$ curl --silent -O https://www.exiv2.org/Stonehenge.jpg +$ exiv2 --verbose --Force rename Stonehenge.jpg +File 1/1: Stonehenge.jpg +Renaming file to ./20150716_153854_1.jpg +``` + +
+ +### **-r** *fmt*, **--rename** *fmt* +Filename format for the [rename](#mv_rename) action (See [TZ](#TZ) +environment variable). The *fmt* string follows the definitions in +[strftime(3)](https://linux.die.net/man/3/strftime), using +[Exif.Photo.DateTimeOriginal](https://www.exiv2.org/tags.html) or +[Exif.Image.DateTime](https://www.exiv2.org/tags.html) as the reference +date and time. In addition, the following special character sequences are +also provided: + +| Variable | Description | +|:------ |:---- | +| :basename: | Original filename without extension | +| :basesuffix: | Suffix in original filename, starts with first dot and ends before extension, e.g. PANO, MP, NIGHT added by Google Camera app | +| :dirname: | Name of the directory holding the original file | +| :parentname: | Name of parent directory | +| :*ExifTagName*: | Placeholder will be replaced by translated value of tag, characters not allowed in file name are replaced by underscore | + +The default *fmt* is %Y%m%d_%H%M%S + +For example, renaming a file when *fmt* is made up of the basename, day, +short month and long year: +``` +$ curl --silent -O https://www.exiv2.org/Stonehenge.jpg +$ exiv2 --verbose --rename ':basename:_%d_%b_%Y' Stonehenge.jpg +File 1/1: Stonehenge.jpg +Renaming file to ./Stonehenge_16_Jul_2015.jpg +``` + +If the filename to rename to already exists and [--Force](#force_Force) +and [--force](#force_Force) are not used, the user is prompted for an +action. + +| Option | Description | +|:------ |:---- | +| [O]verwrite | Rename the file, overriding the existing one | +| [r]ename | Rename the file, but append '_1' ('_2', ...) to the new filename | +| [s]kip : | Cancel renaming this file | + +For example, when renaming a second Stonehenge.jpg file: +``` +$ curl --silent -O https://www.exiv2.org/Stonehenge.jpg +$ exiv2 --verbose --rename ':basename:_%d_%b_%Y' Stonehenge.jpg +File 1/1: Stonehenge.jpg +exiv2.exe: File `./Stonehenge_16_Jul_2015.jpg' exists. [O]verwrite, [r]ename or [s]kip? r +Renaming file to ./Stonehenge_16_Jul_2015_1.jpg +``` + +If the filename contains a suffix, which shall be included in new filename: +``` +$ exiv2 --verbose --rename '%d_%b_%Y:basesuffix:' Stonehenge.PANO.jpg +File 1/1: Stonehenge.PANO.jpg +Renaming file to '16_Jul_2015.PANO'.jpg +``` + +
+ +### **-a** *time*, **--adjust** *time* +Adjusts the times in standard Exif tags, where *time* is in the format: +[+\|-]HH[:MM[:SS]]. This option is only used with the [adjust](#ad_adjust) +action. See [TZ](#TZ) environment variable. + +Examples of *time* are: + +| Time | Description | +|:------ |:---- | +| +1 | Adds one hour | +| 1:01 | Adds one hour and one minute | +| -0:00:30 | Subtracts 30 seconds | + +
+ +### **-Y** *+-n*, **--years** *+-n* +Time adjustment by a positive or negative number of years, for +the [adjust](#ad_adjust) action. See [TZ environment variable](#TZ). + +
+ +### **-O** *+-n*, **--months** *+-n* +Time adjustment by a positive or negative number of months, for +the [adjust](#ad_adjust) action. See [TZ environment variable](#TZ). + +
+ +### **-D** *+-n*, **--days** *+-n* +Time adjustment by a positive or negative number of days, for +the [adjust](#ad_adjust) action. See [TZ environment variable](#TZ). + +