File size: 42,921 Bytes
25ef971
7326d8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25ef971
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Pypdf Migration Check — find imports and show replacement lines</title>
<meta name="description" content="This free browser tool finds PyMuPDF references and shows replacement lines for pypdf, pypdfium2, or pdfplumber.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/pypdf-migration-check"><style data-yj-mfit="1">#out{max-width:100%}#out td,#out th{overflow-wrap:anywhere}#out pre,#out code{white-space:pre-wrap;overflow-wrap:anywhere}@media (max-width:640px){table{display:block;max-width:100%;overflow-x:auto}pre{max-width:100%;overflow-x:auto}code,a{overflow-wrap:anywhere}img,svg,video,canvas,iframe{max-width:100%}textarea,input,select{max-width:100%;box-sizing:border-box}.yj-lgbar{position:static!important;justify-content:flex-end;margin:0 0 10px}h1,h2,h3,h4,p,li,dd,dt,blockquote,figcaption,summary,label,.res,.lead{overflow-wrap:anywhere}label{max-width:100%;box-sizing:border-box}#out table:not(:has(tr>:nth-child(4))){display:table;table-layout:fixed;width:100%}#out table:has(tr>:nth-child(4)),#out table:has(tr>:nth-child(4)) tbody,#out table:has(tr>:nth-child(4)) tr,#out table:has(tr>:nth-child(4)) td{display:block;width:auto;overflow:visible}#out table:has(tr>:nth-child(4)) thead{display:none}#out table:has(tr>:nth-child(4)) tr{padding:10px 0;border-bottom:1px solid rgba(128,128,128,.28)}#out table:has(tr>:nth-child(4)) td{border:0;padding:1px 0}#out table:has(tr>:nth-child(4)) td:nth-child(-n+3){display:inline-block;margin-right:10px;font-size:12px;opacity:.78}}</style>
</head>
<body>
<div class="wrap">
<h1>Pypdf Migration Check — find imports and show replacement lines</h1>
<p class="lede">Finds every PyMuPDF / fitz import, call and requirement line (AGPL-3.0 or paid Artifex licence) and prints the exact pypdf, pypdfium2 or pdfplumber line that replaces it. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">"""Invoice export worker - runs behind the customer portal."""
import fitz
from pathlib import Path


def export(path: str, out_dir: str) -&gt; dict:
    doc = fitz.open(path)
    text = "".join(page.get_text() for page in doc)
    thumb = doc[0].get_pixmap(dpi=110)
    thumb.save(str(Path(out_dir) / "page1.png"))
    hits = doc[-1].search_for("Total due")
    tables = doc[-1].find_tables()
    return {"text": text, "hits": len(hits), "tables": len(tables.tables)}</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Maps every PyMuPDF/fitz line in the open file (imports, calls, requirements, pyproject, environment.yml) to the exact pypdf, pypdfium2 or pdfplumber replacement line.. <strong>In the editor:</strong> Whole-workspace migration plan (each use mapped to its replacement), a dated AGPL exposure report for the company and a CI gate that fails builds adding new PyMuPDF use. — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var PYPDFMIG_RULES = [
 {
  "id": "req-pymupdf",
  "sev": "error",
  "re": "^\\s*(?:-\\s*)?[\"\\']?pymupdf[\"\\']?\\s*(?:[=\u003c\u003e!~;,@\\[\\]\"\\']|$)",
  "flags": "i",
  "what": "PyMuPDF dependency (AGPL-3.0 or paid Artifex commercial licence)",
  "fix": "pypdf\u003e=6.19  (BSD-3-Clause; add pypdfium2\u003e=5.13 to render pages, pdfplumber\u003e=0.11.10 for tables and word positions)",
  "needs_import": false
 },
 {
  "id": "req-pymupdfb",
  "sev": "error",
  "re": "^\\s*(?:-\\s*)?[\"\\']?pymupdfb[\"\\']?\\s*(?:[=\u003c\u003e!~;,@\\[\\]\"\\']|$)",
  "flags": "i",
  "what": "PyMuPDFb dependency (the PyMuPDF binary wheel, AGPL-3.0)",
  "fix": "pypdf\u003e=6.19  (BSD-3-Clause)",
  "needs_import": false
 },
 {
  "id": "req-pymupdf4llm",
  "sev": "error",
  "re": "^\\s*(?:-\\s*)?[\"\\']?pymupdf4llm[\"\\']?\\s*(?:[=\u003c\u003e!~;,@\\[\\]\"\\']|$)",
  "flags": "i",
  "what": "PyMuPDF4LLM dependency (AGPL-3.0 or Artifex commercial, same as PyMuPDF)",
  "fix": "pdfplumber\u003e=0.11.10  (MIT; extract_text / extract_tables per page)",
  "needs_import": false
 },
 {
  "id": "req-pymupdfpro",
  "sev": "warning",
  "re": "^\\s*(?:-\\s*)?[\"\\']?pymupdfpro[\"\\']?\\s*(?:[=\u003c\u003e!~;,@\\[\\]\"\\']|$)",
  "flags": "i",
  "what": "PyMuPDF Pro dependency (commercial licence only; keep it only if your company holds one)",
  "fix": "pypdf\u003e=6.19 for PDF work; Office conversion has no drop-in free replacement",
  "needs_import": false
 },
 {
  "id": "req-fitz",
  "sev": "error",
  "re": "^\\s*(?:-\\s*)?[\"\\']?fitz[\"\\']?\\s*(?:[=\u003c\u003e!~;,@\\[\\]\"\\']|$)",
  "flags": "i",
  "what": "PyPI package 'fitz' is not PyMuPDF (it is an unrelated 0.0.0 project); code doing 'import fitz' is PyMuPDF",
  "fix": "pypdf\u003e=6.19  (and remove the fitz line)",
  "needs_import": false
 },
 {
  "id": "import-fitz",
  "sev": "error",
  "re": "^\\s*import\\s+fitz\\b",
  "flags": "",
  "what": "import fitz loads PyMuPDF (AGPL-3.0 or paid Artifex licence)",
  "fix": "from pypdf import PdfReader, PdfWriter",
  "needs_import": false
 },
 {
  "id": "import-pymupdf",
  "sev": "error",
  "re": "^\\s*import\\s+pymupdf\\b",
  "flags": "",
  "what": "import pymupdf loads PyMuPDF (AGPL-3.0 or paid Artifex licence)",
  "fix": "from pypdf import PdfReader, PdfWriter",
  "needs_import": false
 },
 {
  "id": "from-fitz",
  "sev": "error",
  "re": "^\\s*from\\s+(?:fitz|pymupdf)(?:\\.\\w+)*\\s+import\\b",
  "flags": "",
  "what": "from fitz/pymupdf import ... loads PyMuPDF (AGPL-3.0 or paid Artifex licence)",
  "fix": "from pypdf import PdfReader, PdfWriter",
  "needs_import": false
 },
 {
  "id": "import-pymupdf4llm",
  "sev": "error",
  "re": "^\\s*(?:import|from)\\s+pymupdf4llm\\b",
  "flags": "",
  "what": "pymupdf4llm is AGPL-3.0 or Artifex commercial, like PyMuPDF",
  "fix": "import pdfplumber  # page.extract_text() / page.extract_tables()",
  "needs_import": false
 },
 {
  "id": "langchain-loader",
  "sev": "error",
  "re": "\\bPyMuPDF(?:4LLM)?Loader\\b",
  "flags": "",
  "what": "LangChain PyMuPDFLoader pulls in PyMuPDF (AGPL-3.0)",
  "fix": "from langchain_community.document_loaders import PyPDFLoader",
  "needs_import": false
 },
 {
  "id": "llamaindex-reader",
  "sev": "error",
  "re": "\\bPyMuPDFReader\\b",
  "flags": "",
  "what": "LlamaIndex PyMuPDFReader pulls in PyMuPDF (AGPL-3.0)",
  "fix": "from llama_index.readers.file import PDFReader",
  "needs_import": false
 },
 {
  "id": "fitz-open",
  "sev": "error",
  "re": "\\b(?:fitz|pymupdf)\\.(?:open|Document)\\(",
  "flags": "",
  "what": "fitz.open() opens the file with PyMuPDF",
  "fix": "reader = PdfReader(path)  # new files: writer = PdfWriter()",
  "needs_import": false
 },
 {
  "id": "fitz-matrix",
  "sev": "warning",
  "re": "\\b(?:fitz|pymupdf)\\.(?:Matrix|Rect|Point)\\(",
  "flags": "",
  "what": "fitz.Matrix/Rect/Point geometry is PyMuPDF-only",
  "fix": "pypdfium2 page.render(scale=dpi/72); crop with pdfplumber page.crop((x0, top, x1, bottom))",
  "needs_import": false
 },
 {
  "id": "get-text",
  "sev": "error",
  "re": "\\.get_text\\(",
  "flags": "",
  "what": "page.get_text() is a PyMuPDF call",
  "fix": "page.extract_text()  # pypdf; words with positions: pdfplumber page.extract_words()",
  "needs_import": true
 },
 {
  "id": "get-pixmap",
  "sev": "error",
  "re": "\\.get_pixmap\\(",
  "flags": "",
  "what": "page.get_pixmap() renders with PyMuPDF",
  "fix": "pypdfium2.PdfDocument(path)[i].render(scale=dpi/72).to_pil()",
  "needs_import": true
 },
 {
  "id": "search-for",
  "sev": "error",
  "re": "\\.search_for\\(",
  "flags": "",
  "what": "page.search_for() is a PyMuPDF call",
  "fix": "pdfplumber.open(path).pages[i].search(\"text\")",
  "needs_import": true
 },
 {
  "id": "find-tables",
  "sev": "error",
  "re": "\\.find_tables\\(",
  "flags": "",
  "what": "page.find_tables() is a PyMuPDF call",
  "fix": "pdfplumber.open(path).pages[i].extract_tables()",
  "needs_import": true
 },
 {
  "id": "insert-pdf",
  "sev": "error",
  "re": "\\.insert_pdf\\(",
  "flags": "",
  "what": "doc.insert_pdf() merges with PyMuPDF",
  "fix": "writer = PdfWriter(); writer.append(reader)",
  "needs_import": true
 },
 {
  "id": "page-count",
  "sev": "warning",
  "re": "\\.page_count\\b",
  "flags": "",
  "what": "doc.page_count is a PyMuPDF attribute",
  "fix": "len(reader.pages)",
  "needs_import": true
 },
 {
  "id": "get-toc",
  "sev": "warning",
  "re": "\\.get_toc\\(",
  "flags": "",
  "what": "doc.get_toc() is a PyMuPDF call",
  "fix": "reader.outline",
  "needs_import": true
 },
 {
  "id": "set-metadata",
  "sev": "warning",
  "re": "\\.set_metadata\\(",
  "flags": "",
  "what": "doc.set_metadata() is a PyMuPDF call",
  "fix": "writer.add_metadata({\"/Title\": title})",
  "needs_import": true
 },
 {
  "id": "set-rotation",
  "sev": "warning",
  "re": "\\.set_rotation\\(",
  "flags": "",
  "what": "page.set_rotation() is a PyMuPDF call",
  "fix": "page.rotate(90)  # pypdf",
  "needs_import": true
 },
 {
  "id": "get-images",
  "sev": "warning",
  "re": "\\.get_images\\(",
  "flags": "",
  "what": "page.get_images() is a PyMuPDF call",
  "fix": "page.images  # pypdf, each has .name and .data",
  "needs_import": true
 },
 {
  "id": "insert-text",
  "sev": "warning",
  "re": "\\.insert_(?:text|textbox)\\(",
  "flags": "",
  "what": "page.insert_text() draws with PyMuPDF",
  "fix": "draw on a reportlab canvas, then page.merge_page(overlay_page)  # pypdf",
  "needs_import": true
 },
 {
  "id": "authenticate",
  "sev": "warning",
  "re": "\\.authenticate\\(",
  "flags": "",
  "what": "doc.authenticate() decrypts with PyMuPDF",
  "fix": "reader.decrypt(password)  # pypdf",
  "needs_import": true
 }
];</script>
<script>// PyMuPDF -> pypdf migration map. Same file runs in node (extension, CI) and in the browser page.
(function () {
  var RULES = (typeof module !== 'undefined' && typeof require !== 'undefined') ? require('./rules.json') : window.PYPDFMIG_RULES;
  var COMPILED = RULES.map(function (r) { return { rule: r, re: new RegExp(r.re, r.flags || '') }; });
  var IMPORT_RE = /^\s*(?:import\s+(?:fitz|pymupdf)\b|from\s+(?:fitz|pymupdf)(?:\.\w+)*\s+import\b)/m;
  var CHECKED = Date.UTC(2026, 8, 30); // PyPI versions in the fixes were read on 2026-09-30

  function daysSinceChecked(today) {
    var m = String(today || '').match(/(\d{4})-(\d{2})-(\d{2})/);
    if (!m) return '';
    var t = Date.UTC(+m[1], +m[2] - 1, +m[3]);
    if (isNaN(t)) return '';
    var d = Math.round((t - CHECKED) / 86400000);
    return d > 0 ? ' (versions read on PyPI 2026-09-30, ' + d + ' days before ' + m[0] + ': recheck the pin)' : '';
  }

  function check(text, opts) {
    opts = opts || {};
    var src = String(text == null ? '' : text).replace(/\r\n?/g, '\n');
    var usesPyMuPDF = IMPORT_RE.test(src);
    var tail = daysSinceChecked(opts.today);
    var lines = src.split('\n');
    var findings = [];
    for (var i = 0; i < lines.length; i++) {
      var line = lines[i];
      if (/^\s*#/.test(line) || !line.trim()) continue;
      for (var k = 0; k < COMPILED.length; k++) {
        var c = COMPILED[k];
        if (c.rule.needs_import && !usesPyMuPDF) continue;
        if (!c.re.test(line)) continue;
        findings.push({
          check: c.rule.id, sev: c.rule.sev, line: i + 1,
          msg: c.rule.what + ' -> ' + c.rule.fix + (c.rule.id.indexOf('req-') === 0 ? tail : ''),
          found: line.trim(), fix: c.rule.fix
        });
      }
    }
    return { findings: findings, uses_pymupdf: usesPyMuPDF || findings.length > 0 };
  }

  var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
  if (typeof module !== 'undefined' && module.exports) module.exports = API;
  if (typeof window !== 'undefined') window.PYPDFMIG_ENGINE = API;
})();
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
  const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
  return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
  const f = (res && res.findings) || [];
  const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
  f.forEach(function (x) { lines.push('  ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + '  ' + (x.line ? 'L' + x.line + '  ' : '') + (x.check || x.id || '') + '  ' + (x.msg || x.message || '')); });
  return lines.join('\n');
}
function toHtml(res) {
  const f = (res && res.findings) || [];
  if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
  const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
  return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var PYPDFMIG_REPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = PYPDFMIG_REPORT;
else window.PYPDFMIG_REPORT = PYPDFMIG_REPORT;
</script>
<script>
(function(){
  var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
  today.value=new Date().toISOString().slice(0,10);
  function run(){ try { var res=window.PYPDFMIG_ENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.PYPDFMIG_REPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
  go.onclick=run; if (src.value.trim()) run();
})();
</script>


<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://open-vsx.org/extension/readystack/pypdf-migration-check" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'pypdf-migration-check',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/team" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind pypdf Migration Check - PyMuPDF AGPL Licence Gate every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"pypdf-migration-check",lang:"en",source:"rule_alert"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);document.addEventListener("click",function(ev){var b=ev.target&&ev.target.closest?ev.target.closest("button,input[type=submit]"):null;if(b&&!b.closest("[data-yj],[data-yj-cta],[data-yj-install],[data-yj-alert],.yj-find,.yj-related,#yj-ask,.yjp"))mv(ev);},true);})();</script></section>

<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the full version <span class="yj-price">$29</span></h2><p class="yj-cta-paid" style="margin:.25rem 0 .6rem;color:#1f2a37">Whole-workspace migration plan (each use mapped to its replacement), a dated AGPL exposure report for the company and a CI gate that fails builds adding new PyMuPDF use.</p><p class="yj-cta-pay" style="margin:.25rem 0 1rem;color:#5b6b7c;font-size:.94rem">One payment, one licence key for this tool. The key is shown right after payment.</p><p class="yj-cta-anchor" style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Aspose.PDF for Python via .NET, Developer Small Business: US$1199 (1 developer, 1 location)</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_0pvrLaMj6o8Qtp0GTkTL9p03sJZGqn2iLrElX087SbX" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>

<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-10-01">Updated 2026-10-01</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-10-01","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku431021_result_card.jpg" alt="pypdf Migration Check - PyMuPDF AGPL Licence Gate" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Migrate from PyMuPDF to pypdf: 6 findings in one invoice worker</p>
<p>6 findings in a 13-line invoice export worker: that is what the checker printed for a Python backend team running PyMuPDF behind a customer portal.</p>
<p>What Artifex states</p>
<p>Artifex licenses PyMuPDF under the GNU AGPLv3 or a commercial licence. Its licensing page (artifex.com/licensing) says you cannot deploy the open-source edition as part of a server-based application or service without disclosing your own application&#x27;s full source code under AGPL to any users interacting with it, and that if you cannot meet the AGPL requirements a commercial licence is required. There is no public price on that page. The exposure starts the day the service is deployed to users.</p>
<p>pymupdf4llm, the LLM and RAG helper, is published on PyPI under the same dual licence. So is the binary wheel PyMuPDFb.</p>
<p>What the sample file looked like</p>
<p>The worker opened a PDF, joined the text of every page, rendered page one to a PNG thumbnail, searched the last page for &quot;Total due&quot; and pulled its tables. Six lines touch PyMuPDF:</p>
<p>import fitz  -&gt;  from pypdf import PdfReader, PdfWriter fitz.open(path)  -&gt;  reader = PdfReader(path) page.get_text()  -&gt;  page.extract_text() page.get_pixmap(dpi=110)  -&gt;  pypdfium2.PdfDocument(path)[i].render(scale=dpi/72).to_pil() page.search_for(&quot;Total due&quot;)  -&gt;  pdfplumber.open(path).pages[i].search(&quot;text&quot;) page.find_tables()  -&gt;  pdfplumber.open(path).pages[i].extract_tables()</p>
<p>After the rewrite the same file returns 0 findings. pypdf (BSD-3-Clause) handles reading, text, merging and metadata. pdfplumber (MIT) handles word positions, search and tables. pypdfium2 (BSD-3-Clause / Apache-2.0) renders pages to images. The versions in the fixes were read on PyPI on 2026-09-30: pypdf 6.19.0, pdfplumber 0.11.10, pypdfium2 5.13.0.</p>
<p>Why a package scanner is not enough</p>
<p>pip-licenses and similar tools tell you that pymupdf is AGPL. They stop there. They do not find the six call sites, and they cannot tell you that get_pixmap needs a different library from get_text. Chatbots often answer with PyPDF2, which the project itself retired in favour of pypdf. And the PyPI package literally named fitz is an unrelated 0.0.0 project, so a requirements line reading fitz is a bug on top of a licence question.</p>
<p>The 25 rules</p>
<p>Five dependency rules cover requirements*.txt, pyproject.toml (PEP 621 and Poetry) and conda environment.yml: pymupdf, PyMuPDFb, pymupdf4llm, pymupdfpro and fitz. Six import rules cover import fitz, import pymupdf, from fitz import, pymupdf4llm, the LangChain PyMuPDFLoader and the LlamaIndex PyMuPDFReader. Fourteen call rules cover open, Matrix and Rect, get_text, get_pixmap, search_for, find_tables, insert_pdf, page_count, get_toc, set_metadata, set_rotation, get_images, insert_text and authenticate. Call rules only fire in a file that imports fitz or pymupdf, so a Tkinter get_text call stays quiet.</p>
<p>How to run it</p>
<p>Open the file in VS Code with the extension installed, or paste it into the web version. The Problems panel lists each line with its replacement.</p>
<p>What comes next</p>
<p>A single file is where most people start. The next job is the whole repository: every service, every requirements file, and proof for the company that nobody adds import fitz back next quarter. That is the workspace plan, the dated exposure report and the CI gate.</p>
<p>This is not legal advice. Read the Artifex terms with your counsel before deciding between a commercial licence and a migration.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku431021_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku431021_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does pypdf Migration Check actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads Python files, requirements*.txt, pyproject.toml and environment.yml, finds every PyMuPDF use (import fitz, fitz.open, get_text, get_pixmap, search_for, find_tables and 19 more patterns, 25 rules in total) and prints the exact pypdf, pdfplumber or pypdfium2 line that replaces each one. On the bundled 13-line invoice worker it reports 6 findings, one per PyMuPDF line.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this PyMuPDF to pypdf checker for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Python backend developers and tech leads at companies that run PyMuPDF inside a closed-source product or a SaaS, API or RAG pipeline, for example a FastAPI invoice service or a LangChain ingest job using PyMuPDFLoader. Artifex&#x27;s licensing page says server-based use under AGPL requires disclosing your full source, so these teams either buy a commercial licence or migrate.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just use pip-licenses or ask a chatbot?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">pip-licenses and similar scanners tell you that the pymupdf package is AGPL, then stop. They do not find the 6 call sites in your code or say what replaces get_pixmap or find_tables. Chatbots often suggest PyPDF2, which is deprecated in favour of pypdf. This checker maps each line to a replacement read from PyPI on 2026-09-30.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free, with no key and no limit: the full map for the open file, every finding with its replacement line, in VS Code and in the web version. A licence key adds the whole-workspace migration plan with each use mapped to its replacement, a dated AGPL exposure report for the company, and a CI gate that fails a build adding new PyMuPDF use.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does staying on a commercial PDF licence cost instead?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Artifex does not publish a PyMuPDF commercial price; you contact sales. A comparable commercial library, Aspose.PDF for Python via .NET, lists its Developer Small Business licence at US$1199 for 1 developer and 1 deployment location. pypdf (BSD-3-Clause), pdfplumber (MIT) and pypdfium2 (BSD-3-Clause / Apache-2.0) cost nothing to ship in closed code.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does pypdf Migration Check actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads Python files, requirements*.txt, pyproject.toml and environment.yml, finds every PyMuPDF use (import fitz, fitz.open, get_text, get_pixmap, search_for, find_tables and 19 more patterns, 25 rules in total) and prints the exact pypdf, pdfplumber or pypdfium2 line that replaces each one. On the bundled 13-line invoice worker it reports 6 findings, one per PyMuPDF line."}}, {"@type": "Question", "name": "Who is this PyMuPDF to pypdf checker for?", "acceptedAnswer": {"@type": "Answer", "text": "Python backend developers and tech leads at companies that run PyMuPDF inside a closed-source product or a SaaS, API or RAG pipeline, for example a FastAPI invoice service or a LangChain ingest job using PyMuPDFLoader. Artifex's licensing page says server-based use under AGPL requires disclosing your full source, so these teams either buy a commercial licence or migrate."}}, {"@type": "Question", "name": "Why not just use pip-licenses or ask a chatbot?", "acceptedAnswer": {"@type": "Answer", "text": "pip-licenses and similar scanners tell you that the pymupdf package is AGPL, then stop. They do not find the 6 call sites in your code or say what replaces get_pixmap or find_tables. Chatbots often suggest PyPDF2, which is deprecated in favour of pypdf. This checker maps each line to a replacement read from PyPI on 2026-09-30."}}, {"@type": "Question", "name": "What is free and what needs a licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Free, with no key and no limit: the full map for the open file, every finding with its replacement line, in VS Code and in the web version. A licence key adds the whole-workspace migration plan with each use mapped to its replacement, a dated AGPL exposure report for the company, and a CI gate that fails a build adding new PyMuPDF use."}}, {"@type": "Question", "name": "What does staying on a commercial PDF licence cost instead?", "acceptedAnswer": {"@type": "Answer", "text": "Artifex does not publish a PyMuPDF commercial price; you contact sales. A comparable commercial library, Aspose.PDF for Python via .NET, lists its Developer Small Business licence at US$1199 for 1 developer and 1 deployment location. pypdf (BSD-3-Clause), pdfplumber (MIT) and pypdfium2 (BSD-3-Clause / Apache-2.0) cost nothing to ship in closed code."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:431021,slug:"pypdf-migration-check",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/cinc-migration-check"><span class="yj-rel-t">Cinc Migration Check: Chef Infra licence gate lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/openpdf-migration-check"><span class="yj-rel-t">OpenPDF Migration Check: iText AGPL Gate</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/pekko-migration-gate"><span class="yj-rel-t">Pekko Migration Gate — Akka BSL licence check</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/pyside6-migration-lint"><span class="yj-rel-t">PySide6 Migration Lint: PyQt5/PyQt6 Licence Gate</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/fdroid-inclusion-gate"><span class="yj-rel-t">F-Droid Inclusion Gate - non-free Gradle dependency map</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/ferretdb-migration-lint"><span class="yj-rel-t">FerretDB Migration Lint — MongoDB SSPL gate</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>