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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -5
- data/README.md +94 -54
- data/ext/qpdf_ruby/document_handle.cpp +1 -1
- data/ext/qpdf_ruby/figure_node.cpp +112 -87
- data/ext/qpdf_ruby/mcid_bounds.cpp +317 -0
- data/ext/qpdf_ruby/mcid_bounds.hpp +33 -0
- data/ext/qpdf_ruby/pdf_struct_walker.cpp +11 -18
- data/ext/qpdf_ruby/pdf_struct_walker.hpp +5 -3
- data/ext/qpdf_ruby/pdfua.cpp +909 -0
- data/ext/qpdf_ruby/pdfua.hpp +102 -0
- data/ext/qpdf_ruby/qpdf_ruby.cpp +369 -214
- data/ext/qpdf_ruby/qpdf_ruby.hpp +1 -4
- data/ext/qpdf_ruby/ruby_guard.hpp +71 -0
- data/ext/qpdf_ruby/stroke_bounds.cpp +155 -0
- data/ext/qpdf_ruby/stroke_bounds.hpp +49 -0
- data/ext/qpdf_ruby/xmp_scan.cpp +257 -0
- data/ext/qpdf_ruby/xmp_scan.hpp +27 -0
- data/lib/qpdf_ruby/version.rb +1 -1
- data/lib/qpdf_ruby.rb +3 -2
- data/sig/qpdf_ruby.rbs +78 -1
- metadata +11 -12
- data/.clang-format +0 -4
- data/.rspec +0 -3
- data/.rubocop.yml +0 -88
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/Rakefile +0 -25
- data/cliff.toml +0 -138
- data/docker/Dockerfile +0 -46
- data/ext/qpdf_ruby/pdf_image_mapper.cpp +0 -294
- data/ext/qpdf_ruby/pdf_image_mapper.hpp +0 -68
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.
|
|
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:
|
|
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
data/.rspec
DELETED
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
|
-
}
|