pdf-file-renamer 0.6.2__py3-none-any.whl → 0.6.3__py3-none-any.whl

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.
@@ -3,6 +3,7 @@
3
3
  import asyncio
4
4
  import contextlib
5
5
  import re
6
+ from difflib import SequenceMatcher
6
7
  from pathlib import Path
7
8
 
8
9
  import pdf2doi
@@ -14,10 +15,18 @@ from pdf_file_renamer.domain.ports import DOIExtractor
14
15
  class PDF2DOIExtractor(DOIExtractor):
15
16
  """Extract DOI from PDF files using pdf2doi library."""
16
17
 
17
- def __init__(self) -> None:
18
- """Initialize the PDF2DOI extractor."""
18
+ def __init__(self, validate_match: bool = True, similarity_threshold: float = 0.3) -> None:
19
+ """
20
+ Initialize the PDF2DOI extractor.
21
+
22
+ Args:
23
+ validate_match: Whether to validate that DOI metadata matches PDF content
24
+ similarity_threshold: Minimum similarity score (0-1) for title validation
25
+ """
19
26
  # Suppress pdf2doi verbose output
20
27
  pdf2doi.config.set("verbose", False)
28
+ self.validate_match = validate_match
29
+ self.similarity_threshold = similarity_threshold
21
30
 
22
31
  async def extract_doi(self, pdf_path: Path) -> DOIMetadata | None:
23
32
  """
@@ -91,7 +100,7 @@ class PDF2DOIExtractor(DOIExtractor):
91
100
  # Extract publisher
92
101
  publisher = metadata.get("publisher")
93
102
 
94
- return DOIMetadata(
103
+ doi_metadata = DOIMetadata(
95
104
  doi=identifier,
96
105
  title=title,
97
106
  authors=authors,
@@ -101,6 +110,16 @@ class PDF2DOIExtractor(DOIExtractor):
101
110
  raw_bibtex=validation_info if validation_info else None,
102
111
  )
103
112
 
113
+ # Validate that the DOI metadata matches the PDF content
114
+ if self.validate_match:
115
+ # Extract first page text from PDF to check for title match
116
+ pdf_text = await self._extract_pdf_first_page(pdf_path)
117
+ if not self._validate_doi_matches_pdf(doi_metadata, pdf_text):
118
+ # DOI doesn't match - likely a citation DOI, not the paper's DOI
119
+ return None
120
+
121
+ return doi_metadata
122
+
104
123
  except Exception:
105
124
  # Silently fail - DOI extraction is opportunistic
106
125
  return None
@@ -158,3 +177,120 @@ class PDF2DOIExtractor(DOIExtractor):
158
177
  ]
159
178
 
160
179
  return authors if authors else None
180
+
181
+ async def _extract_pdf_first_page(self, pdf_path: Path) -> str:
182
+ """
183
+ Extract text from the first page of a PDF.
184
+
185
+ Args:
186
+ pdf_path: Path to PDF file
187
+
188
+ Returns:
189
+ Text from first page (empty string if extraction fails)
190
+ """
191
+ try:
192
+ import fitz # PyMuPDF
193
+
194
+ loop = asyncio.get_event_loop()
195
+
196
+ def extract() -> str:
197
+ with fitz.open(pdf_path) as doc:
198
+ if len(doc) > 0:
199
+ return doc[0].get_text()
200
+ return ""
201
+
202
+ return await loop.run_in_executor(None, extract)
203
+ except Exception:
204
+ return ""
205
+
206
+ def _validate_doi_matches_pdf(self, doi_metadata: DOIMetadata, pdf_text: str) -> bool:
207
+ """
208
+ Validate that DOI metadata matches the PDF content.
209
+
210
+ This checks if the title from the DOI metadata appears in the PDF text
211
+ (particularly the first page, where the title should be).
212
+
213
+ Args:
214
+ doi_metadata: DOI metadata to validate
215
+ pdf_text: Text from PDF first page (not full document!)
216
+
217
+ Returns:
218
+ True if metadata appears to match PDF, False otherwise
219
+ """
220
+ if not doi_metadata.title or not pdf_text:
221
+ # If we can't validate, assume it's valid (fail open)
222
+ return True
223
+
224
+ # Normalize text for comparison
225
+ pdf_text_lower = pdf_text.lower()
226
+ title_lower = doi_metadata.title.lower()
227
+
228
+ # Check if the full title appears in the PDF text
229
+ if title_lower in pdf_text_lower:
230
+ return True
231
+
232
+ # Check similarity using SequenceMatcher on first ~300 chars (title area)
233
+ # Most paper titles appear in the first few hundred characters
234
+ title_area = pdf_text_lower[:300]
235
+ similarity = SequenceMatcher(None, title_lower, title_area).ratio()
236
+
237
+ if similarity >= self.similarity_threshold:
238
+ return True
239
+
240
+ # Check if significant words from title appear in the title area ONLY
241
+ # This prevents matching citation DOIs from the references section
242
+ title_words = self._extract_significant_words(title_lower)
243
+ if not title_words:
244
+ return True # Can't validate, fail open
245
+
246
+ # Require at least 70% of significant words to appear in the title area
247
+ matches = sum(1 for word in title_words if word in title_area)
248
+ match_ratio = matches / len(title_words)
249
+
250
+ return match_ratio >= 0.7
251
+
252
+ def _extract_significant_words(self, text: str) -> list[str]:
253
+ """
254
+ Extract significant words from text (removing common words).
255
+
256
+ Args:
257
+ text: Input text
258
+
259
+ Returns:
260
+ List of significant words
261
+ """
262
+ # Common words to skip
263
+ stop_words = {
264
+ "a",
265
+ "an",
266
+ "the",
267
+ "and",
268
+ "or",
269
+ "but",
270
+ "in",
271
+ "on",
272
+ "at",
273
+ "to",
274
+ "for",
275
+ "of",
276
+ "with",
277
+ "by",
278
+ "from",
279
+ "as",
280
+ "is",
281
+ "was",
282
+ "are",
283
+ "were",
284
+ "been",
285
+ "be",
286
+ "this",
287
+ "that",
288
+ "these",
289
+ "those",
290
+ }
291
+
292
+ # Extract words (alphanumeric only)
293
+ words = re.findall(r"\b\w+\b", text.lower())
294
+
295
+ # Filter stop words and short words
296
+ return [w for w in words if w not in stop_words and len(w) > 3]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdf-file-renamer
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: Intelligent PDF renaming using LLMs with DOI-based naming and interactive workflow
5
5
  Project-URL: Homepage, https://github.com/nostoslabs/pdf-renamer
6
6
  Project-URL: Repository, https://github.com/nostoslabs/pdf-renamer
@@ -285,7 +285,8 @@ The tool uses a multi-strategy approach to generate accurate filenames:
285
285
 
286
286
  1. **DOI Detection** (for academic papers)
287
287
  - Searches PDF for DOI identifiers using [pdf2doi](https://github.com/MicheleCotrufo/pdf2doi)
288
- - If found, queries authoritative metadata (title, authors, year, journal)
288
+ - **Validates DOI metadata** against PDF content to prevent citation DOI mismatches
289
+ - If found and validated, queries authoritative metadata (title, authors, year, journal)
289
290
  - Generates filename with **very high confidence** from validated metadata
290
291
  - **Saves API costs** - no LLM call needed for papers with DOIs
291
292
 
@@ -10,7 +10,7 @@ pdf_file_renamer/domain/ports.py,sha256=ebOcHptiOK119NCmIwM32_fbRK5xkZP9K67vjL-4
10
10
  pdf_file_renamer/infrastructure/__init__.py,sha256=C3ZQ7WCPCa6PMfP00lu4wqb0r57GVyDdiD5EL2DhCeY,187
11
11
  pdf_file_renamer/infrastructure/config.py,sha256=baNL5_6_NNiS50ZNdql7fDwQbeAwf6f58HGYIWFQxQQ,2464
12
12
  pdf_file_renamer/infrastructure/doi/__init__.py,sha256=8N9ZEwfG7q5xomzh187YtP8t4CfEBHM334xNRblPeuI,153
13
- pdf_file_renamer/infrastructure/doi/pdf2doi_extractor.py,sha256=714WU8MRQF2mWFEdB9MSm2nexivIByKxciOyArgfkTs,5114
13
+ pdf_file_renamer/infrastructure/doi/pdf2doi_extractor.py,sha256=1tQ7fQF3TPxUZ7By9dzKz4LAfE8TPyjlvt8lACqGiLk,9551
14
14
  pdf_file_renamer/infrastructure/llm/__init__.py,sha256=ToB8__mHvXwaIukGKPEAQ8SeC4ZLiH4auZI1P1yH5PQ,159
15
15
  pdf_file_renamer/infrastructure/llm/pydantic_ai_provider.py,sha256=kVsmj0NIawkj-1WWM0hZXbsNH09GabVZm9HPlYsxGuo,9217
16
16
  pdf_file_renamer/infrastructure/pdf/__init__.py,sha256=uMHqxSXNLZH5WH_e1kXrp9m7uTqPkiI2hXjNo6rCRoo,368
@@ -20,8 +20,8 @@ pdf_file_renamer/infrastructure/pdf/pymupdf_extractor.py,sha256=C61udZCqGqiVx7T0
20
20
  pdf_file_renamer/presentation/__init__.py,sha256=1VR44GoPGTixk3hG5YzhGyQf7a4BTKsJBd2VP3rHcFM,211
21
21
  pdf_file_renamer/presentation/cli.py,sha256=0t_59-utRWLNCYjFetU0ZHoF1DPTjdNiWM9Au0jFaOg,8013
22
22
  pdf_file_renamer/presentation/formatters.py,sha256=8Vz95QupJKkPgPgRyMVmA_gxRWG5vfxdnSd7Czovlrg,8946
23
- pdf_file_renamer-0.6.2.dist-info/METADATA,sha256=qwnly-Ce8cu-S1pNnj6-NwuPC3ZbdOxGTtLheEK0IKc,16851
24
- pdf_file_renamer-0.6.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- pdf_file_renamer-0.6.2.dist-info/entry_points.txt,sha256=0fEGYy60chGE9rECWeCVPxjxzz6vMtIAYdFvmH7xzbw,63
26
- pdf_file_renamer-0.6.2.dist-info/licenses/LICENSE,sha256=_w08V08WgoMpDMlGNlkIatC5QfQ_Ds_rXOBM8pl7ffE,1068
27
- pdf_file_renamer-0.6.2.dist-info/RECORD,,
23
+ pdf_file_renamer-0.6.3.dist-info/METADATA,sha256=ywxT5kRE2VGcv1HUuwvqrAeaVw7ksYsn6Y6MTa5hShA,16952
24
+ pdf_file_renamer-0.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ pdf_file_renamer-0.6.3.dist-info/entry_points.txt,sha256=0fEGYy60chGE9rECWeCVPxjxzz6vMtIAYdFvmH7xzbw,63
26
+ pdf_file_renamer-0.6.3.dist-info/licenses/LICENSE,sha256=_w08V08WgoMpDMlGNlkIatC5QfQ_Ds_rXOBM8pl7ffE,1068
27
+ pdf_file_renamer-0.6.3.dist-info/RECORD,,