pdf-extract — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited pdf-extract (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
KRITICKÉ – MUSÍŠ DODRŽET:
.md soubor a ZASTAV SE.md souboru, nemazej předchozí výsledkyKRITICKÉ: Tento skill zpracovává desítky až stovky stran s obrázky. Aby nedocházelo k vyčerpání kontextového okna:
Kategorie stran podle náročnosti zpracování:
Ověř dostupnost a nainstaluj pokud chybí:
pip3 install PyMuPDF Pillow numpyimport fitz # PyMuPDF
doc = fitz.open("<cesta_k_pdf>")
title = doc.metadata.get("title", "")
author = doc.metadata.get("author", "")
page_count = len(doc)Zpracovávej po dávkách (50–100 stran), aby se šetřila paměť.
#### 3a. Detekce barevných pixelových anotací
Renderuj stránky přes PyMuPDF a hledej barevné pixely:
import numpy as np
from PIL import Image
pix = page.get_pixmap(dpi=100)
img = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.h, pix.w, pix.n)
r, g, b = img[:,:,0], img[:,:,1], img[:,:,2]Detekční pravidla pro jednotlivé barvy:
| Barva | Podmínka | Význam |
|---|---|---|
| Černá | viz krok 3b (vektorová detekce) | Hlavní barva – poznámky, podtržení, zatržení |
| Zelená | (g > 100) & (g > r*1.2) & (g > b*1.2) & ((g-r) > 30) | Zvýraznění, zatržení |
| Žlutá | (r > 150) & (g > 150) & (b < 100) & (r+g > 350) | Highlighter |
| Červená | (r > 150) & (r > g*1.5) & (r > b*1.5) | Důležité, opravy |
| Modrá | (b > 100) & (b > r*1.2) & (b > g*1.2) & ((b-r) > 30) | Poznámky, otázky |
| Oranžová | (r > 180) & (g > 80) & (g < 160) & (b < 80) | Doplňkové zvýraznění |
Práh: > 50 barevných pixelů dané barvy = podezřelá strana
#### 3b. Detekce černých anotací (PRIMÁRNÍ – uživatel píše digitálním perem)
Černé anotace jsou nejčastější typ. Detekce je náročnější, protože tištěný text je také černý.
KLÍČOVÉ ROZLIŠENÍ ruční vs. tištěné: Uživatel píše digitálním perem na tabletu. Jeho tahy jsou vždy nepravidelné – nikdy dokonale rovné čáry, body v nepravidelných rozestupech, mírné odchylky od přímky. Tištěné prvky mají přesnou geometrii – dokonale rovné čáry, přesné úhly, pravidelné rozestupy.
Přístup 1: Vektorové kresby (drawings)
drawings = page.get_drawings()DŮLEŽITÉ – kontroluj OBOJÍ: `color` (stroke) i `fill` (výplň): Některé PDF ukládají ruční anotace jako filled paths (výplň) místo stroked paths (obrys). Mnoho digitálních per generuje color=None, fill=(0,0,0).
import math
def is_hand_drawn(items):
"""Rozliš ruční tah od tištěného prvku.
Ruční tahy (digitální pero na tabletu):
- Mnoho bodů v krátkém úseku (pero sampluje polohu)
- Body NEJSOU na dokonalé přímce – odchylka od spojnice > 0
- Křivky (bezier curves) = téměř jistě ruční
- Celkový tvar je nepravidelný
Tištěné prvky (layout PDF):
- Málo bodů (typicky 2-4 pro čáry a obdélníky)
- Dokonale rovné čáry (odchylka = 0)
- Pravidelné obdélníky, kruhy
- Přesné horizontální/vertikální zarovnání
"""
has_curves = False
line_segments = []
point_count = 0
for item in items:
op = item[0] # typ operace: "l" (line), "c" (curve), "re" (rect), "qu" (quad)
if op == "c": # bezier křivka = téměř jistě ruční
has_curves = True
point_count += 4
elif op == "l": # úsečka
p1, p2 = item[1], item[2]
dx = abs(p2.x - p1.x)
dy = abs(p2.y - p1.y)
length = math.sqrt(dx*dx + dy*dy)
line_segments.append((p1, p2, length, dx, dy))
point_count += 2
elif op == "re": # obdélník = typicky tištěný layout
point_count += 4
elif op == "qu": # quad = typicky tištěný
point_count += 4
# Bezier křivky = ruční
if has_curves:
return True
# Samé obdélníky/quady bez čar = pravděpodobně layout
if not line_segments and not has_curves:
return False
# Mnoho krátkých úseček za sebou = samplovaný tah pera
if len(line_segments) > 5:
short_segments = sum(1 for _, _, l, _, _ in line_segments if l < 15)
if short_segments > len(line_segments) * 0.5:
return True
# Kontrola nepravidelnosti: pokud úsečky NEJSOU dokonale horizontální/vertikální
irregular_count = 0
for _, _, length, dx, dy in line_segments:
if length < 1:
continue
# Dokonale rovná čára: dx==0 nebo dy==0 (nebo téměř)
angle_from_axis = min(dx, dy) / max(dx, dy) if max(dx, dy) > 0 else 0
if angle_from_axis > 0.02: # > 1° odchylka od osy = nepravidelné
irregular_count += 1
if irregular_count > 0 and irregular_count >= len(line_segments) * 0.3:
return True
return False
for d in drawings:
color = d.get("color", None)
fill = d.get("fill", None)
items = d.get("items", [])
is_black_stroke = (color is not None and len(color) >= 3
and color[0] < 0.2 and color[1] < 0.2 and color[2] < 0.2)
is_black_fill = (fill is not None and len(fill) >= 3
and fill[0] < 0.2 and fill[1] < 0.2 and fill[2] < 0.2)
is_bg_rect = (fill is not None and len(fill) >= 3
and fill[0] > 0.9 and fill[1] > 0.9 and fill[2] > 0.9)
if is_bg_rect:
continue # šedé/bílé pozadí citátů – ignorovat
if (is_black_stroke or is_black_fill) and len(items) > 1:
# KLÍČOVÉ: ověř, že jde o ruční tah, ne tištěný prvek
if is_hand_drawn(items):
annotation_items += len(items)Filtrování false positives – pravidla:
Přístup 2: Ink anotace (PDF annotation layer)
for annot in page.annots():
if annot.type[0] in [8, 15]: # Ink, Polyline
# Ink anotace = vždy ruční → zahrnout
passDodatečné filtry pro snížení false positives:
#### 3c. Detekce vestavěných PDF anotací
annots = page.annots()Highlight, Underline, StrikeOut, Text, FreeText, Ink, Square, Circle#### 3d. Klasifikace pozice kreseb na stránce
Pro každou stranu s detekovanými vektorovými kresbami analyzuj POZICI kreseb:
def classify_drawing_position(drawing, page_width):
"""Určí, zda je kresba na okraji stránky nebo v těle textu."""
items = drawing.get("items", [])
xs, ys = [], []
for item in items:
for pt in item[1:]:
if hasattr(pt, 'x'):
xs.append(pt.x)
ys.append(pt.y)
if not xs:
return None, None, None, None
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
x_span = max_x - min_x
y_span = max_y - min_y
# Svislá čára na okraji = zatržení
if x_span < 30: # úzká kresba = svislá čára
if min_x < 60:
return "left_margin", min_x, min_y, max_y
elif min_x > page_width - 60:
return "right_margin", min_x, min_y, max_y
# Široká kresba na okraji
if max_x < 60:
return "left_margin", min_x, min_y, max_y
if min_x > page_width - 60:
return "right_margin", min_x, min_y, max_y
return "body", min_x, min_y, max_yPro každou stranu s potvrzeně ručními kresbami ulož:
#### 3e. Kategorizace nalezených stran
Rozděl nalezené strany do tří skupin:
Jednoduché strany (BEZ vizuální kontroly):
Highlight, Underline, StrikeOutpage.get_text()Marginální strany (programatická extrakce, BEZ vizuální kontroly):
Složité strany (VYŽADUJÍ vizuální kontrolu):
Vypiš uživateli přehled: kolik stran nalezeno, kolik jednoduchých vs. marginálních vs. složitých.
Pro strany s POUZE vestavěnými PDF anotacemi (highlight, underline, strikeout):
for annot in page.annots():
annot_type = annot.type[1] # "Highlight", "Underline", etc.
annot_color = annot.colors.get("stroke", None) or annot.colors.get("fill", None)
# Získej text pod anotací
text = page.get_text("text", clip=annot.rect)Pro strany s vektorovými kresbami POUZE na okrajích stránky. Toto je spolehlivější než vizuální analýza pro zatržení/podtržení, protože fill-only paths se v PNG renderují jako neviditelné.
def get_text_in_y_range(page, y_min, y_max, margin=15):
"""Získej textové řádky v daném rozsahu y-souřadnic.
DŮLEŽITÉ: margin=15 (ne 5!) – kresby často začínají/končí
mírně mimo textový blok, větší margin zajistí správné párování.
"""
blocks = page.get_text("dict")["blocks"]
lines = []
for b in blocks:
if "lines" not in b:
continue
for line in b["lines"]:
ly = (line["bbox"][1] + line["bbox"][3]) / 2 # střed řádku
if y_min - margin <= ly <= y_max + margin:
text = " ".join(span["text"] for span in line["spans"])
if text.strip():
lines.append(text.strip())
return "\n".join(lines)
def merge_ranges(ranges, gap=10):
"""Slučuj překrývající se y-rozsahy.
KRITICKÉ: Řaď podle y_min (x[1]), NE podle position stringu (x[0])!
Špatné řazení způsobí nesprávné sloučení nepřekrývajících se rozsahů
a ztrátu anotací.
"""
if not ranges: return []
ranges.sort(key=lambda x: x[1]) # řaď podle y_min, NE podle position!
merged = [list(ranges[0])]
for pos, y_min, y_max in ranges[1:]:
if y_min <= merged[-1][2] + gap:
merged[-1][2] = max(merged[-1][2], y_max)
else:
merged.append([pos, y_min, y_max])
return merged
# Pro každou marginální stranu:
page = doc[page_num - 1]
pw = page.rect.width
# Seskup kresby podle pozice a y-rozsahu
margin_annotations = [] # [(position, y_min, y_max)]
for d in drawings:
# ... filtruj jen potvrzeně ruční černé kresby ...
pos, min_x, min_y, max_y = classify_drawing_position(d, pw)
if pos in ("left_margin", "right_margin") and (max_y - min_y) > 20:
margin_annotations.append((pos, min_y, max_y))
# Slučuj překrývající se rozsahy (POZOR: merge_ranges musí řadit dle y_min!)
merged = merge_ranges(margin_annotations)
# Pak pro každý rozsah:
for pos, y_min, y_max in merged:
text = get_text_in_y_range(page, y_min, y_max)
# Zapiš jako [ZATRŽENO] s poznámkou o pozici (left/right/oboustranné)Pravidla pro marginální extrakci:
get_text("dict") pro přesné řádky, NE přes get_text("text", clip=...) – clip je méně přesnýKRITICKÉ pro šetření kontextu: Vizuální analýzu NIKDY neděle v hlavním kontextu. Vždy deleguj na Agent subagenty.
#### 4B-1. Příprava PNG souborů
Před spuštěním subagentů exportuj všechny složité strany jako PNG:
pix = page.get_pixmap(dpi=250) # 250 DPI – vyšší rozlišení pro spolehlivou detekci jemných tahů
pix.save(f"temp_pages/page_{page_num:04d}.png")Také pro každou stranu extrahuj tištěný text a drawing metadata:
text = page.get_text()
with open(f"temp_pages/page_{page_num:04d}.txt", "w") as f:
f.write(text)
# Drawing metadata – pomůže subagentovi vědět KAM se dívat
with open(f"temp_pages/page_{page_num:04d}_hints.txt", "w") as f:
for d in page.get_drawings():
fill = d.get("fill", None)
color = d.get("color", None)
items = d.get("items", [])
is_black = (fill and fill[0] < 0.2) or (color and color[0] < 0.2)
if is_black and len(items) > 3:
pos, min_x, min_y, max_y = classify_drawing_position(d, page.rect.width)
if pos:
f.write(f"Drawing: {len(items)} items, pos={pos}, y=[{min_y:.0f}-{max_y:.0f}]\n")#### 4B-2. Spuštění subagentů po dávkách
Rozděl složité strany do dávek po max 15 stranách. Pro každou dávku spusť Agent subagenta:
Agent(
model: "opus", # 1M kontext – unese 15 stran s obrázky
description: "PDF extract batch X",
prompt: """
Jsi expert na extrakci ručních poznámek z PDF knih.
Zpracuj tyto strany: [seznam čísel stran]
Cesta k PNG: temp_pages/page_XXXX.png
Cesta k textu: temp_pages/page_XXXX.txt
Pro každou stranu:
1. Přečti soubor _hints.txt – obsahuje pozice detekovaných kreseb (kde hledat anotace)
2. Přečti PNG přes Read tool (vizuální kontrola)
3. Přečti textový soubor pro kontext tištěného textu
4. Identifikuj typy anotací (viz tabulka níže) – ZAMĚŘ SE na pozice z hints souboru
5. Pokud je na stránce označený obrázek/schéma, extrahuj ho
Typy anotací:
| Tag | Popis |
|-----|-------|
| [RUKOU PSÁNO] | Ruční text – poznámky, nadpisy, komentáře |
| [ZATRŽENO] | Svislá čára na okraji u tištěné pasáže |
| [ZVÝRAZNĚNO] | Highlight / rámeček kolem textu |
| [PODTRŽENO] | Čára pod tištěným textem |
| [OBRÁZEK] | Označený/orámovaný obrázek na stránce |
| [SCHÉMA] | Diagram, schéma, graf – VŽDY zachovat jako obrázek, NIKDY nepřevádět na text |
| [ŠIPKA] | Šipka ukazující na konkrétní místo |
| [SYMBOL] | Hvězdička, vykřičník, otazník, check |
| [ZÁLOŽKA] | Označení celé strany |
DŮLEŽITÉ – rozlišení ručních anotací vs. tištěný obsah:
- Uživatel anotuje digitálním perem na tabletu
- Jeho tahy jsou VŽDY nepravidelné (nikdy dokonale rovné)
- Tištěný text a layout prvky (čáry tabulek, rámečky, dekorativní prvky) jsou dokonale geometrické
- NEOZNAČUJ tištěné prvky jako anotace!
- Hledej: čárky u textu, ohraničení pasáží, podtržení, ruční poznámky na okrajích
- Pokud si nejsi jistý, jestli je prvek ruční nebo tištěný → ZAHRŇ ho do výstupu s poznámkou [nejisté]
SCHÉMATA A DIAGRAMY:
- Pokud strana obsahuje schéma/diagram/graf (tištěný i ručně kreslený), VŽDY ho zachovej jako obrázek
- NIKDY nepřeváděj schéma na textový popis
- Extrahuj oblast schématu jako samostatný PNG pomocí PyMuPDF s clip parametrem:import fitz doc = fitz.open("<cesta_k_pdf>") page = doc[page_num - 1] clip = fitz.Rect(x0, y0, x1, y1) # oblast schématu z hints/vizuální analýzy pix = page.get_pixmap(dpi=300, clip=clip) pix.save(f"{nazev_knihy}_images/img_page{page_num}_{N}.png")
- Vlož do markdown jako obrázek: ``
VÝSTUP: Vrať POUZE strukturovaný markdown pro každou stranu v tomto formátu:
## Strana XX
### [TYP_ANOTACE] emoji_barvy
> text anotace
---
Pokud text nelze přečíst, napiš [nečitelné].
Nevymýšlej text – pokud ho nevidíš, nevymýšlej.
U zatržených pasáží cituj dostatečně velký blok tištěného textu.
U ručních poznámek přepsat text kurzívou.
Schémata a diagramy VŽDY jako obrázek, NIKDY jako text.
Obrázky ukládej jako: {nazev_knihy}_images/img_page{XX}_{N}.png
"""
)Paralelizace: Pokud je stran hodně (30+), spusť 2–3 subagenty paralelně (nezávislé dávky).
#### 4B-3. Extrakce označených obrázků v subagentovi
Subagent v rámci své dávky extrahuje označené obrázky:
images = page.get_images(full=True)
for img_info in images:
xref = img_info[0]
base_image = doc.extract_image(xref)
# Ulož obrázek do výstupní složkyimg_page{XX}_{N}.png do složky {nazev_knihy}_images/KRITICKÉ: Po dokončení subagentů porovnej jejich výsledky s detekčními daty z Fáze 1.
Pro každou stranu, kterou subagent označil jako "false positive" nebo "žádné anotace":
Důvod: Některé PDF ukládají ruční tahy jako color=None, fill=(0,0,0) (filled paths bez stroke). Při renderování do PNG se tyto kresby zobrazí jako extrémně tenké čáry (sub-pixel), které LLM v obrázku nevidí. Vektorová data z PDF jsou v tomto případě autoritativní.
Po dokončení všech subagentů sestav výstupní soubor z:
#### 5a. Název souboru
Nazev_knihy (Autor).mdThinking_Fast_and_Slow (Daniel Kahneman).md#### 5b. Struktura markdown souboru
# Název knihy
**Autor:** Jméno Autora
**Extrahováno:** YYYY-MM-DD
**Zdroj:** název_souboru.pdf
**Počet anotovaných stran:** XX z YY
---
<!-- Uživatelské poznámky – prostor pro vlastní text nad extrakcí -->
---
## Strana 42
### [ZATRŽENO]
> Tištěný text pasáže, která byla zatržena svislou čárou na okraji.
> Může být víceřádková.
### [RUKOU PSÁNO]
> Přepis ručně psané poznámky kurzívou
### [ZVÝRAZNĚNO] 🟡
> Text, který byl zvýrazněn žlutým highlighterem
### [PODTRŽENO]
> Podtržená věta nebo fráze
### [OBRÁZEK]

> Poznámka k obrázku (pokud existuje)
---
## Strana 57
...#### 5c. Pravidla zápisu
>>---rm -rf temp_pages/{nazev_knihy}_images/)Vypiš uživateli:
.md souboruVždy:
[nečitelné]Nikdy:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.