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) -> 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
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 {"&":"&","<":"<",">":">",'"':"""}[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'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 "Total due" and pulled its tables. Six lines touch PyMuPDF:</p>
<p>import fitz -> from pypdf import PdfReader, PdfWriter fitz.open(path) -> reader = PdfReader(path) page.get_text() -> page.extract_text() page.get_pixmap(dpi=110) -> pypdfium2.PdfDocument(path)[i].render(scale=dpi/72).to_pil() page.search_for("Total due") -> pdfplumber.open(path).pages[i].search("text") page.find_tables() -> 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'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>
|