Does anyone know any good tools for editing scanned PDF files offline? Ideally, tools that don't store information.
I tried Sejda, with the OCR converter, then edited it, but I couldn't match the font in the document.
pull down to refresh
Does anyone know any good tools for editing scanned PDF files offline? Ideally, tools that don't store information.
I tried Sejda, with the OCR converter, then edited it, but I couldn't match the font in the document.
Re: the font problem: you can't automagically get a font from OCR, but you can ask an LLM agent to identify the font for you from an image. Maybe, if it is something custom, like on the documents that you're not supposed to edit, you can even get it to make you an opentype font based on the scan. It's not beyond the realm of possibilities.
I identified the font, but it isn't available in Sejda, then I didn't know what to do next.
lmgtfy
I just asked in arena.ai, it gave me a much better result, but still I would like to know your workflow in this.
I'm running a test against stuff I screenshotted from the MLK files, it is still running and I suspect that it will crash lol
Prompt:
1. Identify the characteristics of the font in the image 2. Find opentype fonts that match these characteristics 3. Compare text written in the font with text from the image and identify the closest matchIt crashed.
It wrote this python script tho:
#!/usr/bin/env python3 """ Detailed glyph-level comparison. Render specific phrases in each candidate font, blur to match scan softness, resize to original dimensions, compute per-pixel MSE, and produce a side-by-side grid. """ from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageChops, ImageOps import os, math orig = Image.open("/home/user/uploads/scan.png").convert("L") # Crop the text region roughly (with some margin) - exclude off-white paper background edges ORIG_CROP = orig.crop((10, 5, 855, 365)) W,H = ORIG_CROP.size print("Original crop size:", W, H) # 7 lines, each ~52px tall LINES = [ "At the very outset of the investigation telegrams", "went to all field offices of the Bureau instructing the", "Special Agents in Charge to take personal supervision of", "the investigation, to check out all leads in 24 hours, and", "noting that they would be held personally responsible.", "(HQ 44-38861-153). The files we reviewed show that this", "directive was conscientiously followed. The Bureau sought", ] FONTS = [ ("Courier New Reg", "/home/user/fonts/cour.ttf"), ("Courier New Bold", "/home/user/fonts/courbd.ttf"), ("Courier Prime Reg", "/home/user/fonts/CourierPrime.ttf"), ("Courier Prime Bold", "/home/user/fonts/CourierPrime-Bold.ttf"), ("Special Elite", "/home/user/fonts/SpecialElite.ttf"), ("IBM Plex Mono", "/home/user/fonts/IBMPlexMono.ttf"), ("1942 Report", "/home/user/fonts/1942.ttf"), ("Cutive Mono", "/home/user/fonts/CutiveMono.ttf"), ("Old Typewriter 2.0", "/home/user/fonts/old_tw_dir/Old Typewriter2.0.ttf"), ("Ubuntu Mono Bold", "/home/user/fonts/UbuntuMono-B.ttf"), ("Nimbus Mono Reg", "/home/user/fonts/NimbusMono-Regular.otf"), ("Nimbus Mono Bold", "/home/user/fonts/NimbusMono-Bold.otf"), ("DejaVu Sans Mono", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"), ("DejaVu Sans Mono Bd", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"), ] def render_font(path, size, leading=6, margin_left=20, margin_top=15, inkspread=0, blur=0.0): try: font = ImageFont.truetype(path, size) except Exception as e: return None # measure line heights img_tmp = Image.new("L",(10,10),255) d = ImageDraw.Draw(img_tmp) heights=[] widths=[] for ln in LINES: bb = d.textbbox((0,0), ln, font=font) heights.append(bb[3]-bb[1]) widths.append(bb[2]-bb[0]) line_h = heights[0] w = max(widths)+margin_left*2 h = line_h*7 + leading*6 + margin_top*2 img = Image.new("L",(w,h),255) d = ImageDraw.Draw(img) y = margin_top for i,ln in enumerate(LINES): d.text((margin_left, y), ln, font=font, fill=0) y += line_h + leading # ink spread (dilate) for _ in range(inkspread): img = img.filter(ImageFilter.MaxFilter(3)) if blur > 0: img = img.filter(ImageFilter.GaussianBlur(radius=blur)) return img def mse(a,b): # both same size import numpy as np aa = np.asarray(a, dtype=np.float32) bb = np.asarray(b, dtype=np.float32) diff = (aa-bb)/255.0 return float((diff*diff).mean()) # Search for best parameters per font results = [] for name, path in FONTS: if not os.path.exists(path): continue best = None for size in range(20, 38): for leading in range(0, 12, 2): for ink in range(0, 3): for blur in [0.6, 0.9, 1.2, 1.5]: im = render_font(path, size, leading=leading, inkspread=ink, blur=blur) if im is None: continue im2 = im.resize((W,H), Image.LANCZOS) e = mse(im2, ORIG_CROP) if best is None or e < best[0]: best = (e, size, leading, ink, blur, im2) results.append((best[0], name, best[1:])) print(f"{name:28s} MSE={best[0]:.4f} size={best[1]} leading={best[2]} ink={best[3]} blur={best[4]}") results.sort() print("\n=== Ranking (lower MSE = better match) ===") for e, name, _ in results: print(f" {name:28s} MSE={e:.4f}") # Build side-by-side comparison of the top 6 + original TOP_N = 8 strips = [] lab_h = 30 # Add original strip = Image.new("RGB",(W*2+6, H+lab_h+8),(220,220,220)) d = ImageDraw.Draw(strip) d.text((10,5), "ORIGINAL", fill=(0,0,180)) strip.paste(Image.merge("RGB",(ORIG_CROP,ORIG_CROP,ORIG_CROP)),(2,lab_h+4)) strips.append(("ORIGINAL", strip)) for e, name, (size, leading, ink, blur, im2) in results[:TOP_N]: strip = Image.new("RGB",(W*2+6, H+lab_h+8),(220,220,220)) d = ImageDraw.Draw(strip) d.text((10,5), f"{name} sz={size} lead={leading} ink={ink} blur={blur} MSE={e:.4f}", fill=(150,0,0)) # Left: rendered; Right: side-by-side diff overlay rrgb = Image.merge("RGB",(im2,im2,im2)) strip.paste(rrgb,(2,lab_h+4)) # diff diff = ImageChops.difference(im2, ORIG_CROP) # colorize diff: red channel where diff diff_rgb = Image.merge("RGB",(diff, Image.new("L",diff.size,255), Image.new("L",diff.size,255))) strip.paste(diff_rgb,(W+4,lab_h+4)) strips.append((name, strip)) WW = W*2+6 HH = sum(s.height+6 for _,s in strips) out = Image.new("RGB",(WW,HH),(180,180,180)) y=0 for _,s in strips: out.paste(s,(0,y)); y+=s.height+6 out.save("/home/user/comparison3.png") print("Saved comparison3.png", out.size) # Also save best match image separately best_err, best_name, best_params = results[0] best_size, best_lead, best_ink, best_blur, best_im = best_params print(f"\n==> Best match: {best_name} (MSE={best_err:.4f})")GIving that to the next agent will probably speed it up... iterations... ugh
shouldn't it be something NOT complicated?
Depends. If you want it to lie to you, you ask "whats this" and it will say something random. If you tell it to do a little bit more work, it will go all out forensics and get to the bottom of it.
I personally prefer to be sure of things.
wow, let me try it now!
What would you do if you were me?
The font it was showing in Sejda said "glyphless," so I thought it was the font. 😂 I then tried https://www.whatsthatfont.com/ but couldn't identify the font of the part that I screenshotted. I also tried other tool, the results were so far off, wow and we are in 2026.
I searched online and found JOPDF but although it is free with no paid premium features, the website seems like it was made in china and the software is proprietary.
I tried OthmaneBlial/pdf-editor-offline but had issues getting OCR to work because it wasn’t finding the system installation of Tesseract (at least with the Linux AppImage).