qpdf_ruby 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qpdf_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dieter S.
@@ -130,36 +130,35 @@ extensions:
130
130
  - ext/qpdf_ruby/extconf.rb
131
131
  extra_rdoc_files: []
132
132
  files:
133
- - ".clang-format"
134
- - ".rspec"
135
- - ".rubocop.yml"
136
- - ".ruby-gemset"
137
- - ".ruby-version"
138
133
  - CHANGELOG.md
139
134
  - LICENSE.txt
140
135
  - README.md
141
- - Rakefile
142
- - cliff.toml
143
- - docker/Dockerfile
144
136
  - exe/qpdf_ruby
145
137
  - ext/qpdf_ruby/array_node.cpp
146
138
  - ext/qpdf_ruby/document_handle.cpp
147
139
  - ext/qpdf_ruby/document_handle.hpp
148
140
  - ext/qpdf_ruby/extconf.rb
149
141
  - ext/qpdf_ruby/figure_node.cpp
142
+ - ext/qpdf_ruby/mcid_bounds.cpp
143
+ - ext/qpdf_ruby/mcid_bounds.hpp
150
144
  - ext/qpdf_ruby/mcid_node.cpp
151
145
  - ext/qpdf_ruby/mcr_node.cpp
152
- - ext/qpdf_ruby/pdf_image_mapper.cpp
153
- - ext/qpdf_ruby/pdf_image_mapper.hpp
154
146
  - ext/qpdf_ruby/pdf_struct_walker.cpp
155
147
  - ext/qpdf_ruby/pdf_struct_walker.hpp
148
+ - ext/qpdf_ruby/pdfua.cpp
149
+ - ext/qpdf_ruby/pdfua.hpp
156
150
  - ext/qpdf_ruby/qpdf_ruby.cpp
157
151
  - ext/qpdf_ruby/qpdf_ruby.hpp
152
+ - ext/qpdf_ruby/ruby_guard.hpp
158
153
  - ext/qpdf_ruby/stream_node.cpp
154
+ - ext/qpdf_ruby/stroke_bounds.cpp
155
+ - ext/qpdf_ruby/stroke_bounds.hpp
159
156
  - ext/qpdf_ruby/struct_elem_node.cpp
160
157
  - ext/qpdf_ruby/struct_node.cpp
161
158
  - ext/qpdf_ruby/struct_node.hpp
162
159
  - ext/qpdf_ruby/unknown_node.cpp
160
+ - ext/qpdf_ruby/xmp_scan.cpp
161
+ - ext/qpdf_ruby/xmp_scan.hpp
163
162
  - lib/qpdf_ruby.rb
164
163
  - lib/qpdf_ruby/version.rb
165
164
  - sig/qpdf_ruby.rbs
@@ -186,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
185
  - !ruby/object:Gem::Version
187
186
  version: '0'
188
187
  requirements: []
189
- rubygems_version: 3.6.8
188
+ rubygems_version: 4.0.20
190
189
  specification_version: 4
191
190
  summary: A Ruby interface .
192
191
  test_files: []
data/.clang-format DELETED
@@ -1,4 +0,0 @@
1
- BasedOnStyle: Google
2
- SortIncludes: false
3
- IndentWidth: 2
4
- ColumnLimit: 120
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,88 +0,0 @@
1
- AllCops:
2
- NewCops: enable
3
- TargetRubyVersion: 3.3
4
- Exclude:
5
- - 'ext/qpdf_ruby/extconf.rb'
6
- - 'tmp/**/*'
7
- - 'vendor/**/*'
8
-
9
- Style/StringLiterals:
10
- EnforcedStyle: double_quotes
11
-
12
- Style/StringLiteralsInInterpolation:
13
- EnforcedStyle: double_quotes
14
-
15
- Style/Documentation:
16
- Enabled: false
17
-
18
- RSpec/SubjectStub:
19
- Enabled: false
20
-
21
- RSpec/ExampleLength:
22
- Enabled: false
23
-
24
- Layout/MultilineMethodCallIndentation:
25
- Enabled: false
26
-
27
- Layout/FirstArgumentIndentation:
28
- Enabled: false
29
-
30
- Layout/ClosingParenthesisIndentation:
31
- Enabled: false
32
-
33
- Layout/FirstHashElementIndentation:
34
- EnforcedStyle: consistent
35
-
36
- Layout/FirstArrayElementIndentation:
37
- Enabled: false
38
-
39
- Layout/MultilineOperationIndentation:
40
- Enabled: false
41
-
42
- Layout/BeginEndAlignment:
43
- Enabled: false
44
-
45
- Layout/ArrayAlignment:
46
- Enabled: false
47
-
48
- Layout/LineLength:
49
- Enabled: false
50
-
51
- Layout/LineEndStringConcatenationIndentation:
52
- Enabled: false
53
-
54
- RSpec/MultipleMemoizedHelpers:
55
- Max: 10
56
-
57
- Metrics/MethodLength:
58
- Enabled: false
59
-
60
- Metrics/ClassLength:
61
- Enabled: false
62
-
63
- Metrics/ParameterLists:
64
- Max: 10
65
-
66
-
67
- Gemspec/DevelopmentDependencies:
68
- EnforcedStyle: gemspec
69
-
70
- RSpec/InstanceVariable:
71
- Enabled: false
72
-
73
- RSpec/BeforeAfterAll:
74
- Enabled: false
75
-
76
- RSpec/SpecFilePathFormat:
77
- Enabled: true
78
- Exclude:
79
- - 'spec/acceptance/**/*_spec.rb'
80
-
81
- RSpec/DescribeClass:
82
- Enabled: true
83
- Exclude:
84
- - 'spec/acceptance/**/*_spec.rb'
85
-
86
- plugins:
87
- - rubocop-rake
88
- - rubocop-rspec
data/.ruby-gemset DELETED
@@ -1 +0,0 @@
1
- qpdf_ruby
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- ruby-3.3.4
data/Rakefile DELETED
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require "rubocop/rake_task"
9
-
10
- RuboCop::RakeTask.new
11
-
12
- require "rake/extensiontask"
13
-
14
- desc "Build the gem including native extensions"
15
- task build: :compile
16
-
17
- GEMSPEC = Gem::Specification.load("qpdf_ruby.gemspec")
18
-
19
- Rake::ExtensionTask.new("qpdf_ruby", GEMSPEC) do |ext|
20
- ext.lib_dir = "lib/qpdf_ruby"
21
- end
22
-
23
- Dir.glob("tasks/*.rake").each { |r| load r }
24
-
25
- task default: %i[clobber compile spec rubocop]
data/cliff.toml DELETED
@@ -1,138 +0,0 @@
1
- # git-cliff ~ configuration file
2
- # https://git-cliff.org/docs/configuration
3
-
4
- [changelog]
5
- # template for the changelog header
6
- header = """
7
- <!-- generated by git-cliff start -->
8
- # Changelog\n
9
- All notable changes to this project will be documented in this file.
10
-
11
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
12
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n
13
- """
14
- # template for the changelog body
15
- # https://keats.github.io/tera/docs/#introduction
16
- body = """
17
- {%- macro remote_url() -%}
18
- https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
19
- {%- endmacro -%}
20
-
21
- {% if version -%}
22
- ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
23
- {% else -%}
24
- ## [Unreleased]
25
- {% endif -%}
26
-
27
- {% for group, commits in commits | group_by(attribute="group") %}
28
- ### {{ group | upper_first }}
29
- {%- for commit in commits %}
30
- - {{ commit.message | split(pat="\n") | first | upper_first | trim }}\
31
- {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
32
- {% if commit.remote.pr_number %} in \
33
- [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
34
- {%- endif -%}
35
- {% endfor %}
36
- {% endfor %}
37
-
38
- {%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
39
- ## New Contributors
40
- {%- endif -%}
41
-
42
- {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
43
- * @{{ contributor.username }} made their first contribution
44
- {%- if contributor.pr_number %} in \
45
- [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
46
- {%- endif %}
47
- {%- endfor %}\n
48
- """
49
- # template for the changelog footer
50
- footer = """
51
- {%- macro remote_url() -%}
52
- https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
53
- {%- endmacro -%}
54
-
55
- {% for release in releases -%}
56
- {% if release.version -%}
57
- {% if release.previous.version -%}
58
- [{{ release.version | trim_start_matches(pat="v") }}]: \
59
- {{ self::remote_url() }}/compare/{{ release.previous.version }}..{{ release.version }}
60
- {% endif -%}
61
- {% else -%}
62
- [unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}..HEAD
63
- {% endif -%}
64
- {% endfor %}
65
- <!-- generated by git-cliff end -->
66
- """
67
- # remove the leading and trailing whitespace from the templates
68
- trim = true
69
-
70
- [git]
71
- # parse the commits based on https://www.conventionalcommits.org
72
- conventional_commits = true
73
- # filter out the commits that are not conventional
74
- filter_unconventional = false
75
- # regex for preprocessing the commit messages
76
- commit_preprocessors = [
77
- # remove issue numbers from commits
78
- { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
79
- ]
80
- # regex for parsing and grouping commits
81
- commit_parsers = [
82
- # ✅ Features (additions)
83
- { message = "^feat(?:\\([^)]+\\))?!?:", group = "🚀 Added" },
84
- { message = "^[aA]dd", group = "🚀 Added" },
85
- { message = "^[sS]upport", group = "🚀 Added" },
86
-
87
- # ❌ Removals
88
- { message = "^[rR]emove", group = "🗑️ Removed" },
89
- { message = "^[dD]elete", group = "🗑️ Removed" },
90
-
91
- # 🐛 Fixes
92
- { message = "^fix(?:\\([^)]+\\))?!?:", group = "🐛 Fixed" },
93
- { message = "^[tT]est", group = "🐛 Fixed" },
94
- { message = "^[fF]ix", group = "🐛 Fixed" },
95
-
96
- # 🎨 Refactors
97
- { message = "^refactor(?:\\([^)]+\\))?!?:", group = "🎨 Refactored" },
98
-
99
- # ⚡️ Performance
100
- { message = "^perf(?:\\([^)]+\\))?!?:", group = "⚡️ Performance" },
101
-
102
- # 📝 Docs
103
- { message = "^docs(?:\\([^)]+\\))?!?:", group = "📝 Docs" },
104
-
105
- # 💄 Style (formatting, whitespace, etc.)
106
- { message = "^style(?:\\([^)]+\\))?!?:", group = "💄 Style" },
107
-
108
- # 🧪 Tests
109
- { message = "^test(?:\\([^)]+\\))?!?:", group = "🧪 Tests" },
110
-
111
- # 🔧 Build
112
- { message = "^build(?:\\([^)]+\\))?!?:", group = "🔧 Build" },
113
-
114
- # 🛠️ CI
115
- { message = "^ci(?:\\([^)]+\\))?!?:", skip = true },
116
-
117
- # 🧹 Chores (skip)
118
- { message = "^chore\\(release\\): prepare for", skip = true },
119
- { message = "^chore\\(deps.*\\)", skip = true },
120
- { message = "^chore\\(pr\\)", skip = true },
121
- { message = "^chore\\(pull\\)", skip = true },
122
- { message = "^chore(?:\\([^)]+\\))?!?:", skip = true },
123
- { message = "^\\s*chore", skip = true },
124
-
125
- # ⏪ Reverts
126
- { message = "^revert(?:\\([^)]+\\))?!?:", group = "⏪ Reverted" },
127
-
128
- # 🌀 Catch-all (only if nothing else matched)
129
- { message = "^.*", group = "🔄 Changed" }
130
- ]
131
-
132
-
133
- # filter out the commits that are not matched by commit parsers
134
- filter_commits = false
135
- # sort the tags topologically
136
- topo_order = false
137
- # sort the commits inside sections by oldest/newest order
138
- sort_commits = "newest"
data/docker/Dockerfile DELETED
@@ -1,46 +0,0 @@
1
- FROM ruby:3.3
2
-
3
- ENV DEBIAN_FRONTEND=noninteractive
4
-
5
- RUN set -eux; \
6
- echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list.d/extra.list; \
7
- echo "deb http://deb.debian.org/debian unstable main" >> /etc/apt/sources.list.d/extra.list; \
8
- \
9
- printf "Package: *\nPin: release a=testing\nPin-Priority: 100\n\n" > /etc/apt/preferences.d/99-pin-testing; \
10
- printf "Package: *\nPin: release a=unstable\nPin-Priority: 100\n" >> /etc/apt/preferences.d/99-pin-testing; \
11
- \
12
- apt-get update; \
13
- apt-get -y --no-install-recommends install qpdf libqpdf-dev -t testing; \
14
- apt-mark hold qpdf libqpdf-dev; \
15
- \
16
- apt-get clean; rm -rf /var/lib/apt/lists/*;
17
-
18
- # Smoke‑test (shown in build log)
19
- RUN qpdf --version # should print "qpdf version 12.x"
20
-
21
-
22
- COPY ./pkg/qpdf_ruby-*.gem ./
23
- COPY ./spec/fixtures/example_accessibility.pdf /tmp/dummy.pdf
24
-
25
- RUN gem install ./qpdf_ruby-*.gem -- --with-qpdf-include=/usr/local/include/qpdf --with-qpdf-lib=/usr/local/lib
26
-
27
- RUN ruby -r qpdf_ruby -e 'puts "QpdfRuby version: #{QpdfRuby::VERSION}";doc = QpdfRuby::Document.new "/tmp/dummy.pdf"; puts doc.show_structure'
28
-
29
- # Create a non-root user
30
- RUN set -eux; \
31
- groupadd -r appuser && useradd -r -g appuser -m -d /home/appuser appuser
32
-
33
-
34
- RUN set -eux; \
35
- mkdir -p /workspace
36
-
37
- # Set working directory
38
- WORKDIR /workspace
39
-
40
- RUN chown -R appuser:appuser /workspace
41
-
42
- # Switch to non-root user
43
- USER appuser
44
-
45
- CMD ["/usr/bin/bash"]
46
-
@@ -1,294 +0,0 @@
1
- #include "pdf_image_mapper.hpp"
2
- #include <qpdf/BufferInputSource.hh>
3
- #include <qpdf/QPDFTokenizer.hh>
4
-
5
- #include <algorithm>
6
- #include <optional>
7
-
8
- using Matrix = std::array<double, 6>;
9
-
10
- static std::pair<double, double> apply_matrix(const Matrix& m, double x, double y) {
11
- double x_new = m[0] * x + m[2] * y + m[4];
12
- double y_new = m[1] * x + m[3] * y + m[5];
13
- return {x_new, y_new};
14
- }
15
-
16
- static std::array<double, 4> compute_bbox(double width, double height, const Matrix& matrix) {
17
- std::array<std::pair<double, double>, 4> corners = {apply_matrix(matrix, 0, 0), apply_matrix(matrix, width, 0),
18
- apply_matrix(matrix, width, height),
19
- apply_matrix(matrix, 0, height)};
20
-
21
- double min_x = corners[0].first;
22
- double max_x = corners[0].first;
23
- double min_y = corners[0].second;
24
- double max_y = corners[0].second;
25
-
26
- for (int i = 1; i < 4; ++i) {
27
- min_x = std::min(min_x, corners[i].first);
28
- max_x = std::max(max_x, corners[i].first);
29
- min_y = std::min(min_y, corners[i].second);
30
- max_y = std::max(max_y, corners[i].second);
31
- }
32
-
33
- return {min_x, min_y, max_x, max_y};
34
- }
35
-
36
- static char const* tokenTypeName(QPDFTokenizer::token_type_e ttype) {
37
- // Do this is a case statement instead of a lookup so the compiler
38
- // will warn if we miss any.
39
- switch (ttype) {
40
- case QPDFTokenizer::tt_bad:
41
- return "bad";
42
- case QPDFTokenizer::tt_array_close:
43
- return "array_close";
44
- case QPDFTokenizer::tt_array_open:
45
- return "array_open";
46
- case QPDFTokenizer::tt_brace_close:
47
- return "brace_close";
48
- case QPDFTokenizer::tt_brace_open:
49
- return "brace_open";
50
- case QPDFTokenizer::tt_dict_close:
51
- return "dict_close";
52
- case QPDFTokenizer::tt_dict_open:
53
- return "dict_open";
54
- case QPDFTokenizer::tt_integer:
55
- return "integer";
56
- case QPDFTokenizer::tt_name:
57
- return "name";
58
- case QPDFTokenizer::tt_real:
59
- return "real";
60
- case QPDFTokenizer::tt_string:
61
- return "string";
62
- case QPDFTokenizer::tt_null:
63
- return "null";
64
- case QPDFTokenizer::tt_bool:
65
- return "bool";
66
- case QPDFTokenizer::tt_word:
67
- return "word";
68
- case QPDFTokenizer::tt_eof:
69
- return "eof";
70
- case QPDFTokenizer::tt_space:
71
- return "space";
72
- case QPDFTokenizer::tt_comment:
73
- return "comment";
74
- case QPDFTokenizer::tt_inline_image:
75
- return "inline-image";
76
- }
77
- return nullptr;
78
- }
79
-
80
- static std::optional<ImageInfo> get_image_info(QPDFObjectHandle resources, const std::string& name) {
81
- if (resources.isNull() || !resources.isDictionary()) {
82
- return std::nullopt;
83
- }
84
-
85
- QPDFObjectHandle xobjects = resources.getKey("/XObject");
86
- if (xobjects.isNull() || !xobjects.isDictionary()) {
87
- return std::nullopt;
88
- }
89
-
90
- if (!xobjects.hasKey(name)) {
91
- return std::nullopt;
92
- }
93
-
94
- QPDFObjectHandle xobject = xobjects.getKey(name);
95
- if (!xobject.isStream()) {
96
- return std::nullopt;
97
- }
98
-
99
- QPDFObjectHandle dict = xobject.getDict();
100
- bool is_image =
101
- (dict.hasKey("/Subtype") && dict.getKey("/Subtype").isName() && dict.getKey("/Subtype").getName() == "/Image");
102
-
103
- if (is_image) {
104
- ImageInfo image_info;
105
-
106
- if (dict.hasKey("/Height") && dict.getKey("/Height").isInteger()) {
107
- image_info.height = dict.getKey("/Height").getIntValue();
108
- }
109
-
110
- if (dict.hasKey("/Width") && dict.getKey("/Width").isInteger()) {
111
- image_info.width = dict.getKey("/Width").getIntValue();
112
- }
113
-
114
- return image_info;
115
- }
116
-
117
- return std::nullopt;
118
- ;
119
- }
120
-
121
- PDFImageMapper::PDFImageMapper(int target_mcid) : target_mcid(target_mcid), in_target_mcid(false) {}
122
-
123
- void PDFImageMapper::push_cm(double value) {
124
- if (cm_fixed_size_queue.size() == 6) {
125
- cm_fixed_size_queue.pop_front(); // Remove oldest element
126
- }
127
- cm_fixed_size_queue.push_back(value);
128
- }
129
-
130
- void PDFImageMapper::find(QPDF& pdf) {
131
- QPDFPageDocumentHelper doc_helper(pdf);
132
- std::vector<QPDFPageObjectHelper> pages = doc_helper.getAllPages();
133
- for (auto& page : pages) {
134
- QPDFPageObjectHelper poh(page);
135
-
136
- auto crop_box = poh.getCropBox();
137
-
138
- this->find(poh);
139
- }
140
- }
141
-
142
- struct OperatorInfo {
143
- const char* name;
144
- const char* description;
145
- int operand_count;
146
- };
147
-
148
- static constexpr OperatorInfo OPERATOR_CM = {"cm", "Concatenate matrix (set CTM)", 6};
149
- static constexpr OperatorInfo OPERATOR_Q = {"q", "Save graphics state", 0};
150
- static constexpr OperatorInfo OPERATOR_Q_UPPER = {"Q", "Restore graphics state", 0};
151
- static constexpr OperatorInfo OPERATOR_DO = {"Do", "Invoke named XObject (draw image/form)", 1};
152
- static constexpr OperatorInfo OPERATOR_BDC = {"BDC", "Begin Marked Content sequence with property list", 2};
153
- static constexpr OperatorInfo OPERATOR_EMC = {"EMC", "End Marked Content sequence", 0};
154
-
155
- class CMDoExtractor : public QPDFObjectHandle::ParserCallbacks {
156
- public:
157
- explicit CMDoExtractor(QPDFPageObjectHelper& page_ref) : page(page_ref) {
158
- // You can add any other initialization logic here if needed
159
- }
160
-
161
- std::vector<double> current_matrix = {1, 0, 0, 1, 0, 0};
162
- std::stack<std::vector<double>> matrix_stack;
163
- std::vector<QPDFObjectHandle> operand_stack;
164
-
165
- std::stack<int> mcid_stack;
166
- int current_mcid;
167
-
168
- void handleEOF() override {
169
- // No action needed for this example
170
- }
171
-
172
- std::optional<ImageInfo> image_info(std::string imgName) {
173
- QPDFObjectHandle resources = page.getObjectHandle().getKey("/Resources");
174
-
175
- return get_image_info(resources, imgName);
176
- }
177
-
178
- void handleObject(QPDFObjectHandle obj, size_t offset, size_t length) override {
179
- if (obj.isOperator()) {
180
- std::string op = obj.getOperatorValue();
181
-
182
- if (op == OPERATOR_CM.name && operand_stack.size() >= OPERATOR_CM.operand_count) {
183
- auto numeric_at = [&](std::size_t distance_from_top) -> double {
184
- auto& obj = operand_stack[operand_stack.size() - distance_from_top];
185
- auto t = obj.getTypeName(); // "integer", "real", …
186
- if (std::strcmp(t, "integer") != 0 && std::strcmp(t, "real") != 0) {
187
- std::stringstream ss;
188
- ss << "numeric operand expected (got " << t << ")";
189
- throw std::runtime_error(ss.str());
190
- }
191
- return obj.getNumericValue();
192
- };
193
-
194
- // pull operands (top of stack is distance 1)
195
- double f = numeric_at(1);
196
- double e = numeric_at(2);
197
- double d = numeric_at(3);
198
- double c = numeric_at(4);
199
- double b = numeric_at(5);
200
- double a = numeric_at(6);
201
- current_matrix = {a, b, c, d, e, f};
202
-
203
- operand_stack.resize(operand_stack.size() - 6);
204
- } else if (op == OPERATOR_Q.name) {
205
- matrix_stack.push(current_matrix);
206
- } else if (op == OPERATOR_Q_UPPER.name) {
207
- if (!matrix_stack.empty()) {
208
- current_matrix = matrix_stack.top();
209
- matrix_stack.pop();
210
- }
211
- } else if (op == OPERATOR_BDC.name && operand_stack.size() >= OPERATOR_BDC.operand_count) {
212
- mcid_stack.push(current_mcid); // Save current MCID
213
-
214
- QPDFObjectHandle properties_obj = operand_stack.back(); // Properties operand
215
- // QPDFObjectHandle tag_obj = operand_stack[operand_stack.size() - 2]; // Tag operand
216
-
217
- int mcid_val = -1; // Default if MCID not found or not an integer
218
- if (properties_obj.isDictionary()) {
219
- if (properties_obj.hasKey("/MCID") && properties_obj.getKey("/MCID").isInteger()) {
220
- mcid_val = properties_obj.getKey("/MCID").getIntValue();
221
- }
222
- } else if (properties_obj.isName()) {
223
- // Properties is a name, look it up in Resources /Properties dictionary
224
- QPDFObjectHandle resources = page.getObjectHandle().getKey("/Resources");
225
- if (resources.isDictionary() && resources.hasKey("/Properties")) {
226
- QPDFObjectHandle properties_dict = resources.getKey("/Properties");
227
- if (properties_dict.isDictionary() && properties_dict.hasKey(properties_obj.getName())) {
228
- QPDFObjectHandle actual_props = properties_dict.getKey(properties_obj.getName());
229
- if (actual_props.isDictionary() && actual_props.hasKey("/MCID") &&
230
- actual_props.getKey("/MCID").isInteger()) {
231
- mcid_val = actual_props.getKey("/MCID").getIntValue();
232
- }
233
- }
234
- }
235
- }
236
- current_mcid = mcid_val;
237
- operand_stack.resize(operand_stack.size() - OPERATOR_BDC.operand_count);
238
- } else if (op == OPERATOR_EMC.name) { // EMC takes 0 operands
239
- if (!mcid_stack.empty()) {
240
- current_mcid = mcid_stack.top();
241
- mcid_stack.pop();
242
- } else {
243
- current_mcid = -1; // Reset to default if stack underflow (should be balanced)
244
- }
245
- // EMC takes 0 operands, so no change to operand_stack based on operand_count
246
- } else if (op == OPERATOR_DO.name && !operand_stack.empty()) {
247
- std::string imgName = operand_stack.back().getName();
248
- auto image_info = this->image_info(imgName);
249
-
250
- std::copy(current_matrix.begin(), current_matrix.end(), image_info->cm_matrix.begin());
251
-
252
- image_info->mcid = current_mcid;
253
-
254
- image_info->bbox = compute_bbox(image_info->width, image_info->height, image_info->cm_matrix);
255
-
256
- QPDFObjectHandle mb = page.getObjectHandle().getKey("/MediaBox");
257
-
258
- double px0 = mb.getArrayItem(0).getNumericValue();
259
- double py0 = mb.getArrayItem(1).getNumericValue();
260
- double px1 = mb.getArrayItem(2).getNumericValue();
261
- double py1 = mb.getArrayItem(3).getNumericValue();
262
-
263
- auto& bb = image_info->bbox; // shorthand reference
264
- bb[0] = std::max(bb[0], px0); // left edge
265
- bb[1] = std::max(bb[1], py0); // bottom edge
266
- bb[2] = std::min(bb[2], px1); // right edge
267
- bb[3] = std::min(bb[3], py1); // top edge
268
-
269
- image_to_mcid[imgName] = *image_info;
270
-
271
- operand_stack.pop_back();
272
- } else {
273
- // For other operators, just clear the operand stack
274
- operand_stack.clear();
275
- }
276
- } else {
277
- operand_stack.push_back(obj);
278
- }
279
- }
280
-
281
- const std::map<std::string, ImageInfo>& getImageMap() const { return image_to_mcid; }
282
-
283
- private:
284
- QPDFPageObjectHelper& page;
285
- std::map<std::string, ImageInfo> image_to_mcid;
286
- };
287
-
288
- void PDFImageMapper::find(QPDFPageObjectHelper& page) {
289
- CMDoExtractor cb(page);
290
- page.parseContents(&cb);
291
-
292
- const auto& extracted_map = cb.getImageMap();
293
- image_to_mcid.insert(extracted_map.begin(), extracted_map.end());
294
- }