[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7C724ZMS8CVVRGDFPF64H5C":3,"skill-same:01M7C724ZMS8CVVRGDFPF64H5C":45},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":27,"repositoryHtmlUrl":28,"repositoryHomepageUrl":6,"ownerHtmlUrl":29,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":30,"install":31,"resources":44},"01M7C724ZMS8CVVRGDFPF64H5C","image-ocr",null,"Extract image text with Tesseract and resolve receipt date glyph ambiguities, rounded payable totals, and evidence-preserving field updates.","linyouxue\u002Fskill-repair-benchmark","linyouxue","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F216151464?v=4","01M7BJEVCXKD74KH6FG9CBVSQB","林宥学","01M7BJEVE02NP217V93BKZFC0J",0,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fevaluation\u002Fresults\u002F对于jpg-ocr-stat的尝试\u002Fskills\u002Fbundles\u002Ftest-skillaxe-date-round26-opus80-20261012\u002Finputs\u002Fround-26\u002Fskills\u002Fimage-ocr\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: image-ocr\ndescription: Extract image text with Tesseract and resolve receipt date glyph ambiguities, rounded payable totals, and evidence-preserving field updates.\n---\n\n# Image OCR Skill\n\n## Purpose\nThis skill enables accurate text extraction from image files (JPG, PNG, etc.) using Tesseract OCR via the `pytesseract` Python library. It is suitable for scanned documents, screenshots, photos of text, receipts, forms, and other visual content containing text.\n\nFor structured receipt fields, follow **Structured Field Decision Branches** below\nafter the initial extraction. Encode the applicable branches in the extraction\nand update logic before accepting field values; the raw OCR examples are not a\ncomplete field-selection algorithm.\n\n## When to Use\n- Extracting text from scanned documents or photos\n- Reading text from screenshots or image captures\n- Processing batch image files that contain textual information\n- Converting visual documents to machine-readable text\n- Extracting structured data from forms, receipts, or tables in images\n\n## Required Libraries\n\nThe following Python libraries are required:\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\n```\n\n## Input Requirements\n- **File formats**: JPG, JPEG, PNG, WEBP\n- **Image quality**: Minimum 300 DPI recommended for printed text; clear and legible text\n- **File size**: Under 5MB per image (resize if necessary)\n- **Text language**: Specify if non-English to improve accuracy\n\n## Output Schema\nThe raw OCR helper returns JSON conforming to this schema. For a caller-requested\nspreadsheet, preserve the caller's final schema and keep auxiliary evidence in a\nseparate working file:\n\n```json\n{\n  \"success\": true,\n  \"filename\": \"example.jpg\",\n  \"extracted_text\": \"Full raw text extracted from the image...\",\n  \"confidence\": \"high|medium|low\",\n  \"metadata\": {\n    \"language_detected\": \"en\",\n    \"text_regions\": 3,\n    \"has_tables\": false,\n    \"has_handwriting\": false\n  },\n  \"warnings\": [\n    \"Text partially obscured in bottom-right corner\",\n    \"Low contrast detected in header section\"\n  ]\n}\n```\n\n\n### Field Descriptions\n\n- `success`: Boolean indicating whether text extraction completed\n- `filename`: Original image filename\n- `extracted_text`: Complete text content in reading order (top-to-bottom, left-to-right)\n- `confidence`: Overall OCR confidence level based on image quality and text clarity\n- `metadata.language_detected`: ISO 639-1 language code\n- `metadata.text_regions`: Number of distinct text blocks identified\n- `metadata.has_tables`: Whether tabular data structures were detected\n- `metadata.has_handwriting`: Whether handwritten text was detected\n- `warnings`: Array of quality issues or potential errors\n\n\n## Code Examples\n\n### Basic OCR Extraction\n\n```python\nimport pytesseract\nfrom PIL import Image\n\ndef extract_text_from_image(image_path):\n    \"\"\"Extract text from a single image using Tesseract OCR.\"\"\"\n    img = Image.open(image_path)\n    text = pytesseract.image_to_string(img)\n    return text.strip()\n```\n\n### OCR with Confidence Data\n\n```python\nimport pytesseract\nfrom PIL import Image\n\ndef extract_with_confidence(image_path):\n    \"\"\"Extract text with per-word confidence scores.\"\"\"\n    img = Image.open(image_path)\n\n    # Get detailed OCR data including confidence\n    data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)\n\n    words = []\n    confidences = []\n\n    for i, word in enumerate(data['text']):\n        if word.strip():  # Skip empty strings\n            words.append(word)\n            confidences.append(data['conf'][i])\n\n    # Calculate average confidence\n    avg_confidence = sum(c for c in confidences if c > 0) \u002F len([c for c in confidences if c > 0]) if confidences else 0\n\n    return {\n        'text': ' '.join(words),\n        'average_confidence': avg_confidence,\n        'word_count': len(words)\n    }\n```\n\n### Full OCR with JSON Output\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\n\ndef ocr_to_json(image_path):\n    \"\"\"Perform OCR and return results as JSON.\"\"\"\n    filename = os.path.basename(image_path)\n    warnings = []\n\n    try:\n        img = Image.open(image_path)\n\n        # Get detailed OCR data\n        data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)\n\n        # Extract text preserving structure\n        text = pytesseract.image_to_string(img)\n\n        # Calculate confidence\n        confidences = [c for c in data['conf'] if c > 0]\n        avg_conf = sum(confidences) \u002F len(confidences) if confidences else 0\n\n        # Determine confidence level\n        if avg_conf >= 80:\n            confidence = \"high\"\n        elif avg_conf >= 50:\n            confidence = \"medium\"\n        else:\n            confidence = \"low\"\n            warnings.append(f\"Low OCR confidence: {avg_conf:.1f}%\")\n\n        # Count text regions (blocks)\n        block_nums = set(data['block_num'])\n        text_regions = len([b for b in block_nums if b > 0])\n\n        result = {\n            \"success\": True,\n            \"filename\": filename,\n            \"extracted_text\": text.strip(),\n            \"confidence\": confidence,\n            \"metadata\": {\n                \"language_detected\": \"en\",\n                \"text_regions\": text_regions,\n                \"has_tables\": False,\n                \"has_handwriting\": False\n            },\n            \"warnings\": warnings\n        }\n\n    except Exception as e:\n        result = {\n            \"success\": False,\n            \"filename\": filename,\n            \"extracted_text\": \"\",\n            \"confidence\": \"low\",\n            \"metadata\": {\n                \"language_detected\": \"unknown\",\n                \"text_regions\": 0,\n                \"has_tables\": False,\n                \"has_handwriting\": False\n            },\n            \"warnings\": [f\"OCR failed: {str(e)}\"]\n        }\n\n    return result\n\n# Usage\nresult = ocr_to_json(\"document.jpg\")\nprint(json.dumps(result, indent=2))\n```\n\n### Batch Processing Multiple Images\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\nfrom pathlib import Path\n\ndef process_image_directory(directory_path, output_file):\n    \"\"\"Process all images in a directory and save results.\"\"\"\n    image_extensions = {'.jpg', '.jpeg', '.png', '.webp'}\n    results = []\n\n    for file_path in sorted(Path(directory_path).iterdir()):\n        if file_path.suffix.lower() in image_extensions:\n            result = ocr_to_json(str(file_path))\n            results.append(result)\n            print(f\"Processed: {file_path.name}\")\n\n    # Save results\n    with open(output_file, 'w') as f:\n        json.dump(results, f, indent=2)\n\n    return results\n```\n\n\n## Tesseract Configuration Options\n\n### Language Selection\n\n```python\n# Specify language (default is English)\ntext = pytesseract.image_to_string(img, lang='eng')\n\n# Multiple languages\ntext = pytesseract.image_to_string(img, lang='eng+fra+deu')\n```\n\n### Page Segmentation Modes (PSM)\n\nUse `--psm` to control how Tesseract segments the image:\n\n```python\n# PSM 3: Fully automatic page segmentation (default)\ntext = pytesseract.image_to_string(img, config='--psm 3')\n\n# PSM 4: Assume single column of text\ntext = pytesseract.image_to_string(img, config='--psm 4')\n\n# PSM 6: Assume uniform block of text\ntext = pytesseract.image_to_string(img, config='--psm 6')\n\n# PSM 11: Sparse text - find as much text as possible\ntext = pytesseract.image_to_string(img, config='--psm 11')\n```\n\nCommon PSM values:\n- `0`: Orientation and script detection (OSD) only\n- `3`: Fully automatic page segmentation (default)\n- `4`: Single column of text of variable sizes\n- `6`: Uniform block of text\n- `7`: Single text line\n- `11`: Sparse text\n- `13`: Raw line\n\n\n## Image Preprocessing\n\nFor better OCR accuracy, preprocess images:\n\n```python\nfrom PIL import Image, ImageFilter, ImageOps\n\ndef preprocess_image(image_path):\n    \"\"\"Preprocess image for better OCR results.\"\"\"\n    img = Image.open(image_path)\n\n    # Convert to grayscale\n    img = img.convert('L')\n\n    # Increase contrast\n    img = ImageOps.autocontrast(img)\n\n    # Apply slight sharpening\n    img = img.filter(ImageFilter.SHARPEN)\n\n    return img\n\n# Use preprocessed image for OCR\nimg = preprocess_image(\"document.jpg\")\ntext = pytesseract.image_to_string(img)\n```\n\n### Advanced Preprocessing Strategies\n\nFor difficult images (low contrast, faded text, dark backgrounds), try multiple preprocessing approaches:\n\n1. **Grayscale + Autocontrast** - Basic enhancement for most images\n2. **Inverted** - Use `ImageOps.invert()` for dark backgrounds with light text\n3. **Scaling** - Upscale small images (e.g., 2x or 3x) before OCR to improve character recognition. For faint or thin fonts on receipts, 3x with autocontrast frequently recovers decimal points and punctuation that 1x\u002F2x lose.\n4. **Thresholding** - Convert to binary using `img.point(lambda p: 255 if p > threshold else 0)` with different threshold values (e.g., 100, 128)\n5. **Sharpening** - Apply `ImageFilter.SHARPEN` to improve edge clarity\n\n\n## Multi-Pass OCR Strategy\n\nFor challenging images, a single OCR pass may miss text. Use multiple passes with different configurations:\n\n1. **Try multiple PSM modes** - Different page segmentation modes work better for different layouts (e.g., `--psm 6` for blocks, `--psm 4` for columns, `--psm 11` for sparse text)\n\n2. **Try multiple preprocessing variants** - Run OCR on several preprocessed versions of the same image\n\n3. **Retain results** - Keep each pass's text and source separately for structured-field decisions. Concatenation can improve raw-text coverage, but it loses the evidence needed to select one field value.\n\n```python\ndef multi_pass_ocr(image_path):\n    \"\"\"Run OCR with multiple strategies and combine results.\"\"\"\n    img = Image.open(image_path)\n    gray = ImageOps.grayscale(img)\n    w, h = img.size\n\n    # Generate preprocessing variants (include an upscaled variant; 3x often\n    # recovers thin punctuation such as the decimal point in prices)\n    variants = [\n        ImageOps.autocontrast(gray),\n        ImageOps.invert(ImageOps.autocontrast(gray)),\n        gray.filter(ImageFilter.SHARPEN),\n        ImageOps.autocontrast(img.resize((w * 3, h * 3), Image.LANCZOS).convert('L')),\n    ]\n\n    # PSM modes to try\n    psm_modes = ['--psm 6', '--psm 4', '--psm 11']\n\n    all_text = []\n    for variant in variants:\n        for psm in psm_modes:\n            try:\n                text = pytesseract.image_to_string(variant, config=psm)\n                if text.strip():\n                    all_text.append(text)\n            except Exception:\n                pass\n\n    # Combine all extracted text\n    return \"\\n\".join(all_text)\n```\n\nThis approach improves extraction for receipts, faded documents, and images with varying quality.\n\n### Structured Field Decision Branches\n\nAfter one batch extraction, save provisional results and a separate evidence\nrecord containing each candidate's raw text, field label, image region, and OCR\nsource. Evaluate the following triggers for every field, including non-empty and\nformat-valid fields. Select a branch before selecting a value. A clear field can\nbe confirmed directly; OCR frequency, keyword rank, and variant order alone are\nnot confirmation evidence.\n\n#### Branch A — Date glyph inspection before confirmation\n\n**Trigger:** an unconfirmed date has `8` in the day or month of any recognized\ncandidate. Check the day\u002Fmonth components of every raw and normalized candidate;\nexclude the year from this digit-only gate. A standalone `3`, or `8` occurring\nonly in the year, does not automatically require viewing. For a matching day or\nmonth, actually open this receipt's date image before confirmation even when OCR\nis high-confidence, variants agree, and the date is calendar-valid. Queue each\ndate once; reuse its recorded inspection when its confirmed value is unchanged.\n\n```python\ndef date_requires_image_review(*day_month_candidates):\n    \"\"\"Pass parsed (day, month) pairs, never complete dates or year tokens.\"\"\"\n    return any(\n        \"8\" in str(day) or \"8\" in str(month)\n        for day, month in day_month_candidates\n    )\n```\n\nAny date still requires this branch when a component has broken strokes, unclear\nloop closure, contact with a separator, conflicting readings, or unresolved\nday\u002Fmonth\u002Fyear order. This includes ambiguities in the year and digit confusions\nother than `3`\u002F`8`. A changed candidate goes through step 4 rather than silently\nreplacing a confirmed value. A nearby `Time` line can help locate the block and\nprovide same-font comparison digits.\n\n**Execute in this order:**\n1. Preserve the current raw date and each alternative. Crop the complete `Date`\n   line, including its label, separators, day, month, year, and margins; retain the\n   neighboring `Time` line when present. Save the original-pixel crop and a larger\n   nearest-neighbor copy. Use these as the reference; sharpened, thresholded, or\n   smoothed versions can manufacture or erase a connecting stroke.\n2. Actually open the saved crop with the image-view tool, such as\n   `file_editor(command=\"view\", path=\"\u003Csaved crop path>\")`. Saving a PNG or\n   printing its OCR text does not complete this step. Viewing another receipt or\n   an unrelated image does not clear this date's gate. Read the entire date from\n   this receipt's actual crop, not just the triggering day\u002Fmonth digit. A queued\n   date requires this actual view even when the candidate seems unambiguous.\n   Check the glyphs:\n\n   | Visible feature in the original crop | Interpretation to check |\n   | --- | --- |\n   | Two rightward-curving lobes joined on the right, with open gaps on the left | An open-arc digit such as `3`; do not treat the lobes as two closed rings |\n   | A real left-hand connecting stroke closes both the upper and lower holes | A closed-loop digit such as `8`; verify both closures in the original pixels |\n   | Apparent closure exists only after thresholding, scaling, or isolated noise bridges a gap | Processing artifact; return to the original crop and keep alternatives pending |\n\n   Compare the glyph with unambiguous printed digits in the same font, including\n   the neighboring time when useful. The time supplies glyph shapes, not a date\n   value. Inspect all complete date components rather than choosing a digit from its\n   overall dark outline. Neither a global `8 -> 3` replacement nor the reverse\n   is a valid repair.\n3. Use focused line OCR only to generate additional candidate readings. Resolve\n   the actual printed glyph using the crop and its label context, then establish\n   day\u002Fmonth\u002Fyear order and convert to ISO. Write a separate date-evidence record\n   with the accepted value, crop path, visible distinguishing strokes, and status\n   `confirmed`. If the original pixels cannot distinguish the candidates, retain\n   status `pending`; format validity, first-variant order, and votes cannot promote\n   it to `confirmed`.\n4. Load that evidence record on every later extraction. A `confirmed` date is the\n   accepted date for the deliverable, independent of new OCR order or votes. Put\n   any disagreeing new reading into review without overwriting it. Replace the\n   accepted date only after another actual crop inspection supplies stronger\n   same-field evidence, and record the reason. A provisional date is not protected\n   merely because it happened to appear in an earlier workbook.\n5. Commit a newly confirmed or corrected date before moving to another receipt:\n   persist the accepted value and evidence, update that receipt's date in the\n   working rows, and actually save the target workbook. If it already exists,\n   update the affected cell while preserving unrelated accepted fields; otherwise\n   create it from the current working rows. Reopen the workbook and read back the\n   changed cell. Editing an override in a script is only preparation: execute the\n   targeted update and verify the saved value now, not after the remaining review\n   queue. This commit needs no new full-batch OCR.\n\n**Confirm when:** the image-view operation for this receipt's date has occurred,\nthe printed-date reading, relevant stroke evidence, and complete date context are\nrecorded, and workbook readback matches the accepted date. Reuse that recorded\ninspection for an unchanged confirmed date; a changed reading requires the\nreview in step 4. When no image-view tool is available, acknowledge that limitation\nand keep a triggered date pending rather than claiming a visual check. Keep the\nevidence record outside the requested workbook schema.\n\n#### Branch B — Inclusive total, rounding, and a later final total\n\n**Trigger:** the payment block contains a pre-round total (possibly labeled as\ntax-inclusive), a signed rounding adjustment, and a later `TOTAL`, `NET TOTAL`,\n`NETT TOTAL`, or other final-payable line. Different amounts in these roles are\nnot conflicting OCR readings of one field.\n\n**Execute:**\n1. Crop and read the whole payment block together. Assign candidates to separate\n   roles: subtotal, tax\u002Fservice components, pre-round payable, signed adjustment,\n   final payable, tendered cash, and change. Keep a tax-summary table outside this\n   payment block; its own `TOTAL` is not the amount due.\n2. Preserve the `+` or `-` on the adjustment. Parse these monetary tokens with\n   `Decimal` and check `final_payable == pre_round_payable + signed_adjustment`.\n   The sign is part of the value, not punctuation to discard. If the pre-round\n   payable already includes tax\u002Fservice charges, use those components only to\n   cross-check it, not to add them again.\n3. Select the explicitly printed final-payable amount when the field association\n   and arithmetic agree, even if an earlier tax-inclusive amount occurs in more\n   OCR passes or has a higher-ranked keyword. The final label can be larger or\n   bolder than surrounding text; inspect that line separately if OCR misses it.\n4. If a digit or separator is missing or the arithmetic disagrees, review the\n   corresponding local crop. Accept the result only after resolving the reading;\n   do not invent a rounding adjustment or silently force the arithmetic to fit.\n\n**Confirm when:** the final amount is anchored to its own label and verified\nagainst the printed settlement relation. If no rounding\u002Ffinal-total pattern is\npresent, resolve the final-payable role from the receipt's actual labels; do not\napply this branch's adjustment speculatively. Keywords such as `GST` identify a\npossible component, not an automatic reason to discard an inclusive payable.\n\n#### Branch C — Amount split across lines or repaired digits\n\n**Trigger:** an amount keyword has no number on its line, or a candidate relies\non inserting a decimal, removing an OCR character, or joining separated digits.\n\n**Execute:** crop the label and neighboring lines together. Locate the numeric\nregion that is visually associated with that label; exclude quantity, date,\nidentifier, tax-rate, and change fields. Use next-line\u002Ftrailing tokens only as\ncandidates. Support each punctuation\u002Fdigit repair from the original monetary\nregion, and then apply Branch B if settlement roles are present.\n\n**Confirm when:** both the label association and repaired number are supported.\nNeither the last number on a line nor successful parsing clears this branch.\n\n#### Branch D — Reusing confirmed fields after a parser change\n\n**Trigger:** changing shared parsing\u002Fselection logic, adding a raw-OCR preference,\nor retrying extraction after any fields have been confirmed.\n\n**Execute:** save and reload the confirmed field values with their evidence before\nthe change. Run the new logic into provisional candidates. Merge only reviewed\nupdates to unresolved or demonstrably affected fields; retain every other\nconfirmed field. A changed candidate requires stronger same-field evidence and\na recorded reason before replacing a confirmed value. Rebuild the deliverable\nfrom this merged accepted state, not from fresh unreviewed batch output.\n\n**Confirm when:** a before\u002Fafter field comparison shows that every replacement\nhas been reviewed. Execute the write, reopen the destination workbook, and compare\nits stored values with the accepted state. Logs or values left only in memory do\nnot demonstrate delivery. Reserve budget for this final readback instead of\nunnecessarily repeating full-batch OCR at the end. Reserve at least the last\nfive available model\u002Ftool iterations for delivery: save the merged accepted\nstate, execute any still-unapplied targeted updates, reopen the actual output,\nand compare its stored fields, coverage, schema, and ordering with that state.\nCheck the remaining budget before starting another OCR experiment or group of\nimage views. Once the delivery reserve is reached, finish writing and readback\ninstead of starting optional rechecks. If required fields remain unresolved,\nsave the current work and report them rather than guessing or claiming completion.\n\n### Numeric Normalization for Receipts and Forms\n\nOCR frequently corrupts decimal separators and digit spacing in monetary amounts.\nApply the following repairs only inside a monetary region associated with its\nlabel, and only when its printed evidence supports the repair (Branch C):\n\n- Replace `:` between digits with `.` (e.g., `92:80` -> `92.80`).\n- Collapse a single space between a 1-4 digit group and a 2-digit group into `.` (e.g., `92 80` -> `92.80`, `1 234 56` -> keep the comma-thousands intact but treat trailing ` 56` as decimals only when no other decimal separator is present).\n- Reinterpret bare digit clusters of length >=3 whose last two digits look like cents as `X.YY` when no decimal separator was recognized (e.g., `9280` -> `92.80`) **only** when the line contains a strong keyword such as `GRAND TOTAL` and no better candidate exists.\n- Remove clearly extraneous surrounding OCR marks; retain decimal\u002Fthousands punctuation and any genuine sign, especially on rounding adjustments.\n- Accept commas as thousands separators: `1,234.56` -> `1234.56`.\n\nPrefer a well-formed monetary token in the identified field. Next-line candidates\nand decimal repairs must pass Branch C; this helper parses a token but does not\ndecide whether it is the final amount. Keep date and ID parsing separate.\n\n```python\nimport re\nfrom decimal import Decimal\n\nMONEY_STRICT = re.compile(r'[+-]?(?:\\d{1,3}(?:,\\d{3})+|\\d+)\\.\\d{2}')\n\ndef parse_money(token: str, *, decimal_repair_supported=False):\n    \"\"\"Parse a field-associated monetary token, preserving a printed sign.\"\"\"\n    if token is None:\n        return None\n    s = token.strip().replace('\\u2212', '-')\n    if MONEY_STRICT.fullmatch(s):\n        return Decimal(s.replace(',', ''))\n    if not decimal_repair_supported:\n        return None\n    # Enable only after the original region establishes the decimal position.\n    candidates = [\n        re.sub(r'(\\d):(\\d{2})$', r'\\1.\\2', s),\n        re.sub(r'(\\d)\\s+(\\d{2})$', r'\\1.\\2', s),\n    ]\n    if re.fullmatch(r'[+-]?\\d{3,}', s):\n        candidates.append(s[:-2] + '.' + s[-2:])\n    for candidate in candidates:\n        if MONEY_STRICT.fullmatch(candidate):\n            return Decimal(candidate.replace(',', ''))\n    return None\n```\n\n\n## Error Handling\n\n### Common Issues and Solutions\n\n**Issue**: Tesseract not found\n\n```python\n# Verify Tesseract is installed\ntry:\n    pytesseract.get_tesseract_version()\nexcept pytesseract.TesseractNotFoundError:\n    print(\"Tesseract is not installed or not in PATH\")\n```\n\n**Issue**: Poor OCR quality\n\n- Preprocess image (grayscale, contrast, sharpen)\n- Use appropriate PSM mode for the document type\n- Ensure image resolution is sufficient (300+ DPI)\n\n**Issue**: Empty or garbage output\n\n- Check if image contains actual text\n- Try different PSM modes\n- Verify image is not corrupted\n\n**Issue**: Keyword detected but no numeric value parsed\n\n- Do not immediately return null. First:\n  1. Run Branch C on the label and its neighboring numeric region.\n  2. If multiple settlement amounts or rounding appear, run Branch B.\n  3. Retain unresolved candidates and continue focused review within the available budget.\n- If the field remains unresolved, report extraction failure instead of accepting\n  an unrelated but parseable number. Non-empty ambiguous fields use the same\n  decision branches; review is not limited to null values.\n\n\n## Quality Self-Check\n\nBefore returning results, verify:\n\n- [ ] Output is valid JSON (use `json.loads()` to validate)\n- [ ] All required fields are present (`success`, `filename`, `extracted_text`, `confidence`, `metadata`)\n- [ ] Text preserves logical reading order\n- [ ] Confidence level reflects actual OCR quality\n- [ ] Warnings array includes all detected issues\n- [ ] Special characters are properly escaped in JSON\n\n### Structured-Value Extraction Self-Check\n\nWhen the caller extracts specific fields (amounts, dates, IDs), also verify:\n\n- [ ] For every image where a target keyword (e.g., `GRAND TOTAL`, `TOTAL RM`) is detected in **any** OCR variant, a candidate value was produced, or the reason for failure is explicitly logged.\n- [ ] Every unconfirmed date with `8` in a candidate day\u002Fmonth passed Branch A with an actual view; year-only `8` and standalone `3` did not trigger unnecessary viewing, while ambiguous or changed dates still received review.\n- [ ] Each accepted date correction was saved and read back before continuing the review queue; unchanged confirmed dates reused their recorded inspection.\n- [ ] Settlement totals passed Branch B where applicable; pre-round, rounded, tax-summary, tendered, and change amounts were kept in separate roles.\n- [ ] Decimal repairs and split-line candidates passed Branch C, preserving a genuine adjustment sign.\n- [ ] Parser changes and retries passed Branch D; confirmed fields were preserved unless an evidenced correction was accepted.\n- [ ] The delivery reserve was used to apply all accepted updates to the requested file, and readback matched the accepted fields and caller's schema, coverage, and ordering.\n\n\n## Limitations\n\n- Tesseract works best with printed text; handwriting recognition is limited\n- Accuracy decreases with decorative fonts, artistic text, or extreme stylization\n- Mathematical equations and special notation may not extract accurately\n- Redacted or watermarked text cannot be recovered\n- Severe image degradation (blur, noise, low resolution) reduces accuracy\n- Complex multi-column layouts may require custom PSM configuration\n- Thin punctuation (decimal points, colons) is often lost or substituted; always attempt numeric normalization before rejecting a value\n\n\n## Version History\n\n- **1.0.0** (2026-01-13): Initial release with Tesseract\u002Fpytesseract OCR\n- **1.1.0**: Added value-level voting across variants, numeric normalization guidance for OCR-corrupted amounts, and structured-value extraction self-check.\n","# Image OCR Skill\n\n## Purpose\nThis skill enables accurate text extraction from image files (JPG, PNG, etc.) using Tesseract OCR via the `pytesseract` Python library. It is suitable for scanned documents, screenshots, photos of text, receipts, forms, and other visual content containing text.\n\nFor structured receipt fields, follow **Structured Field Decision Branches** below\nafter the initial extraction. Encode the applicable branches in the extraction\nand update logic before accepting field values; the raw OCR examples are not a\ncomplete field-selection algorithm.\n\n## When to Use\n- Extracting text from scanned documents or photos\n- Reading text from screenshots or image captures\n- Processing batch image files that contain textual information\n- Converting visual documents to machine-readable text\n- Extracting structured data from forms, receipts, or tables in images\n\n## Required Libraries\n\nThe following Python libraries are required:\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\n```\n\n## Input Requirements\n- **File formats**: JPG, JPEG, PNG, WEBP\n- **Image quality**: Minimum 300 DPI recommended for printed text; clear and legible text\n- **File size**: Under 5MB per image (resize if necessary)\n- **Text language**: Specify if non-English to improve accuracy\n\n## Output Schema\nThe raw OCR helper returns JSON conforming to this schema. For a caller-requested\nspreadsheet, preserve the caller's final schema and keep auxiliary evidence in a\nseparate working file:\n\n```json\n{\n  \"success\": true,\n  \"filename\": \"example.jpg\",\n  \"extracted_text\": \"Full raw text extracted from the image...\",\n  \"confidence\": \"high|medium|low\",\n  \"metadata\": {\n    \"language_detected\": \"en\",\n    \"text_regions\": 3,\n    \"has_tables\": false,\n    \"has_handwriting\": false\n  },\n  \"warnings\": [\n    \"Text partially obscured in bottom-right corner\",\n    \"Low contrast detected in header section\"\n  ]\n}\n```\n\n\n### Field Descriptions\n\n- `success`: Boolean indicating whether text extraction completed\n- `filename`: Original image filename\n- `extracted_text`: Complete text content in reading order (top-to-bottom, left-to-right)\n- `confidence`: Overall OCR confidence level based on image quality and text clarity\n- `metadata.language_detected`: ISO 639-1 language code\n- `metadata.text_regions`: Number of distinct text blocks identified\n- `metadata.has_tables`: Whether tabular data structures were detected\n- `metadata.has_handwriting`: Whether handwritten text was detected\n- `warnings`: Array of quality issues or potential errors\n\n\n## Code Examples\n\n### Basic OCR Extraction\n\n```python\nimport pytesseract\nfrom PIL import Image\n\ndef extract_text_from_image(image_path):\n    \"\"\"Extract text from a single image using Tesseract OCR.\"\"\"\n    img = Image.open(image_path)\n    text = pytesseract.image_to_string(img)\n    return text.strip()\n```\n\n### OCR with Confidence Data\n\n```python\nimport pytesseract\nfrom PIL import Image\n\ndef extract_with_confidence(image_path):\n    \"\"\"Extract text with per-word confidence scores.\"\"\"\n    img = Image.open(image_path)\n\n    # Get detailed OCR data including confidence\n    data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)\n\n    words = []\n    confidences = []\n\n    for i, word in enumerate(data['text']):\n        if word.strip():  # Skip empty strings\n            words.append(word)\n            confidences.append(data['conf'][i])\n\n    # Calculate average confidence\n    avg_confidence = sum(c for c in confidences if c > 0) \u002F len([c for c in confidences if c > 0]) if confidences else 0\n\n    return {\n        'text': ' '.join(words),\n        'average_confidence': avg_confidence,\n        'word_count': len(words)\n    }\n```\n\n### Full OCR with JSON Output\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\n\ndef ocr_to_json(image_path):\n    \"\"\"Perform OCR and return results as JSON.\"\"\"\n    filename = os.path.basename(image_path)\n    warnings = []\n\n    try:\n        img = Image.open(image_path)\n\n        # Get detailed OCR data\n        data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)\n\n        # Extract text preserving structure\n        text = pytesseract.image_to_string(img)\n\n        # Calculate confidence\n        confidences = [c for c in data['conf'] if c > 0]\n        avg_conf = sum(confidences) \u002F len(confidences) if confidences else 0\n\n        # Determine confidence level\n        if avg_conf >= 80:\n            confidence = \"high\"\n        elif avg_conf >= 50:\n            confidence = \"medium\"\n        else:\n            confidence = \"low\"\n            warnings.append(f\"Low OCR confidence: {avg_conf:.1f}%\")\n\n        # Count text regions (blocks)\n        block_nums = set(data['block_num'])\n        text_regions = len([b for b in block_nums if b > 0])\n\n        result = {\n            \"success\": True,\n            \"filename\": filename,\n            \"extracted_text\": text.strip(),\n            \"confidence\": confidence,\n            \"metadata\": {\n                \"language_detected\": \"en\",\n                \"text_regions\": text_regions,\n                \"has_tables\": False,\n                \"has_handwriting\": False\n            },\n            \"warnings\": warnings\n        }\n\n    except Exception as e:\n        result = {\n            \"success\": False,\n            \"filename\": filename,\n            \"extracted_text\": \"\",\n            \"confidence\": \"low\",\n            \"metadata\": {\n                \"language_detected\": \"unknown\",\n                \"text_regions\": 0,\n                \"has_tables\": False,\n                \"has_handwriting\": False\n            },\n            \"warnings\": [f\"OCR failed: {str(e)}\"]\n        }\n\n    return result\n\n# Usage\nresult = ocr_to_json(\"document.jpg\")\nprint(json.dumps(result, indent=2))\n```\n\n### Batch Processing Multiple Images\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\nfrom pathlib import Path\n\ndef process_image_directory(directory_path, output_file):\n    \"\"\"Process all images in a directory and save results.\"\"\"\n    image_extensions = {'.jpg', '.jpeg', '.png', '.webp'}\n    results = []\n\n    for file_path in sorted(Path(directory_path).iterdir()):\n        if file_path.suffix.lower() in image_extensions:\n            result = ocr_to_json(str(file_path))\n            results.append(result)\n            print(f\"Processed: {file_path.name}\")\n\n    # Save results\n    with open(output_file, 'w') as f:\n        json.dump(results, f, indent=2)\n\n    return results\n```\n\n\n## Tesseract Configuration Options\n\n### Language Selection\n\n```python\n# Specify language (default is English)\ntext = pytesseract.image_to_string(img, lang='eng')\n\n# Multiple languages\ntext = pytesseract.image_to_string(img, lang='eng+fra+deu')\n```\n\n### Page Segmentation Modes (PSM)\n\nUse `--psm` to control how Tesseract segments the image:\n\n```python\n# PSM 3: Fully automatic page segmentation (default)\ntext = pytesseract.image_to_string(img, config='--psm 3')\n\n# PSM 4: Assume single column of text\ntext = pytesseract.image_to_string(img, config='--psm 4')\n\n# PSM 6: Assume uniform block of text\ntext = pytesseract.image_to_string(img, config='--psm 6')\n\n# PSM 11: Sparse text - find as much text as possible\ntext = pytesseract.image_to_string(img, config='--psm 11')\n```\n\nCommon PSM values:\n- `0`: Orientation and script detection (OSD) only\n- `3`: Fully automatic page segmentation (default)\n- `4`: Single column of text of variable sizes\n- `6`: Uniform block of text\n- `7`: Single text line\n- `11`: Sparse text\n- `13`: Raw line\n\n\n## Image Preprocessing\n\nFor better OCR accuracy, preprocess images:\n\n```python\nfrom PIL import Image, ImageFilter, ImageOps\n\ndef preprocess_image(image_path):\n    \"\"\"Preprocess image for better OCR results.\"\"\"\n    img = Image.open(image_path)\n\n    # Convert to grayscale\n    img = img.convert('L')\n\n    # Increase contrast\n    img = ImageOps.autocontrast(img)\n\n    # Apply slight sharpening\n    img = img.filter(ImageFilter.SHARPEN)\n\n    return img\n\n# Use preprocessed image for OCR\nimg = preprocess_image(\"document.jpg\")\ntext = pytesseract.image_to_string(img)\n```\n\n### Advanced Preprocessing Strategies\n\nFor difficult images (low contrast, faded text, dark backgrounds), try multiple preprocessing approaches:\n\n1. **Grayscale + Autocontrast** - Basic enhancement for most images\n2. **Inverted** - Use `ImageOps.invert()` for dark backgrounds with light text\n3. **Scaling** - Upscale small images (e.g., 2x or 3x) before OCR to improve character recognition. For faint or thin fonts on receipts, 3x with autocontrast frequently recovers decimal points and punctuation that 1x\u002F2x lose.\n4. **Thresholding** - Convert to binary using `img.point(lambda p: 255 if p > threshold else 0)` with different threshold values (e.g., 100, 128)\n5. **Sharpening** - Apply `ImageFilter.SHARPEN` to improve edge clarity\n\n\n## Multi-Pass OCR Strategy\n\nFor challenging images, a single OCR pass may miss text. Use multiple passes with different configurations:\n\n1. **Try multiple PSM modes** - Different page segmentation modes work better for different layouts (e.g., `--psm 6` for blocks, `--psm 4` for columns, `--psm 11` for sparse text)\n\n2. **Try multiple preprocessing variants** - Run OCR on several preprocessed versions of the same image\n\n3. **Retain results** - Keep each pass's text and source separately for structured-field decisions. Concatenation can improve raw-text coverage, but it loses the evidence needed to select one field value.\n\n```python\ndef multi_pass_ocr(image_path):\n    \"\"\"Run OCR with multiple strategies and combine results.\"\"\"\n    img = Image.open(image_path)\n    gray = ImageOps.grayscale(img)\n    w, h = img.size\n\n    # Generate preprocessing variants (include an upscaled variant; 3x often\n    # recovers thin punctuation such as the decimal point in prices)\n    variants = [\n        ImageOps.autocontrast(gray),\n        ImageOps.invert(ImageOps.autocontrast(gray)),\n        gray.filter(ImageFilter.SHARPEN),\n        ImageOps.autocontrast(img.resize((w * 3, h * 3), Image.LANCZOS).convert('L')),\n    ]\n\n    # PSM modes to try\n    psm_modes = ['--psm 6', '--psm 4', '--psm 11']\n\n    all_text = []\n    for variant in variants:\n        for psm in psm_modes:\n            try:\n                text = pytesseract.image_to_string(variant, config=psm)\n                if text.strip():\n                    all_text.append(text)\n            except Exception:\n                pass\n\n    # Combine all extracted text\n    return \"\\n\".join(all_text)\n```\n\nThis approach improves extraction for receipts, faded documents, and images with varying quality.\n\n### Structured Field Decision Branches\n\nAfter one batch extraction, save provisional results and a separate evidence\nrecord containing each candidate's raw text, field label, image region, and OCR\nsource. Evaluate the following triggers for every field, including non-empty and\nformat-valid fields. Select a branch before selecting a value. A clear field can\nbe confirmed directly; OCR frequency, keyword rank, and variant order alone are\nnot confirmation evidence.\n\n#### Branch A — Date glyph inspection before confirmation\n\n**Trigger:** an unconfirmed date has `8` in the day or month of any recognized\ncandidate. Check the day\u002Fmonth components of every raw and normalized candidate;\nexclude the year from this digit-only gate. A standalone `3`, or `8` occurring\nonly in the year, does not automatically require viewing. For a matching day or\nmonth, actually open this receipt's date image before confirmation even when OCR\nis high-confidence, variants agree, and the date is calendar-valid. Queue each\ndate once; reuse its recorded inspection when its confirmed value is unchanged.\n\n```python\ndef date_requires_image_review(*day_month_candidates):\n    \"\"\"Pass parsed (day, month) pairs, never complete dates or year tokens.\"\"\"\n    return any(\n        \"8\" in str(day) or \"8\" in str(month)\n        for day, month in day_month_candidates\n    )\n```\n\nAny date still requires this branch when a component has broken strokes, unclear\nloop closure, contact with a separator, conflicting readings, or unresolved\nday\u002Fmonth\u002Fyear order. This includes ambiguities in the year and digit confusions\nother than `3`\u002F`8`. A changed candidate goes through step 4 rather than silently\nreplacing a confirmed value. A nearby `Time` line can help locate the block and\nprovide same-font comparison digits.\n\n**Execute in this order:**\n1. Preserve the current raw date and each alternative. Crop the complete `Date`\n   line, including its label, separators, day, month, year, and margins; retain the\n   neighboring `Time` line when present. Save the original-pixel crop and a larger\n   nearest-neighbor copy. Use these as the reference; sharpened, thresholded, or\n   smoothed versions can manufacture or erase a connecting stroke.\n2. Actually open the saved crop with the image-view tool, such as\n   `file_editor(command=\"view\", path=\"\u003Csaved crop path>\")`. Saving a PNG or\n   printing its OCR text does not complete this step. Viewing another receipt or\n   an unrelated image does not clear this date's gate. Read the entire date from\n   this receipt's actual crop, not just the triggering day\u002Fmonth digit. A queued\n   date requires this actual view even when the candidate seems unambiguous.\n   Check the glyphs:\n\n   | Visible feature in the original crop | Interpretation to check |\n   | --- | --- |\n   | Two rightward-curving lobes joined on the right, with open gaps on the left | An open-arc digit such as `3`; do not treat the lobes as two closed rings |\n   | A real left-hand connecting stroke closes both the upper and lower holes | A closed-loop digit such as `8`; verify both closures in the original pixels |\n   | Apparent closure exists only after thresholding, scaling, or isolated noise bridges a gap | Processing artifact; return to the original crop and keep alternatives pending |\n\n   Compare the glyph with unambiguous printed digits in the same font, including\n   the neighboring time when useful. The time supplies glyph shapes, not a date\n   value. Inspect all complete date components rather than choosing a digit from its\n   overall dark outline. Neither a global `8 -> 3` replacement nor the reverse\n   is a valid repair.\n3. Use focused line OCR only to generate additional candidate readings. Resolve\n   the actual printed glyph using the crop and its label context, then establish\n   day\u002Fmonth\u002Fyear order and convert to ISO. Write a separate date-evidence record\n   with the accepted value, crop path, visible distinguishing strokes, and status\n   `confirmed`. If the original pixels cannot distinguish the candidates, retain\n   status `pending`; format validity, first-variant order, and votes cannot promote\n   it to `confirmed`.\n4. Load that evidence record on every later extraction. A `confirmed` date is the\n   accepted date for the deliverable, independent of new OCR order or votes. Put\n   any disagreeing new reading into review without overwriting it. Replace the\n   accepted date only after another actual crop inspection supplies stronger\n   same-field evidence, and record the reason. A provisional date is not protected\n   merely because it happened to appear in an earlier workbook.\n5. Commit a newly confirmed or corrected date before moving to another receipt:\n   persist the accepted value and evidence, update that receipt's date in the\n   working rows, and actually save the target workbook. If it already exists,\n   update the affected cell while preserving unrelated accepted fields; otherwise\n   create it from the current working rows. Reopen the workbook and read back the\n   changed cell. Editing an override in a script is only preparation: execute the\n   targeted update and verify the saved value now, not after the remaining review\n   queue. This commit needs no new full-batch OCR.\n\n**Confirm when:** the image-view operation for this receipt's date has occurred,\nthe printed-date reading, relevant stroke evidence, and complete date context are\nrecorded, and workbook readback matches the accepted date. Reuse that recorded\ninspection for an unchanged confirmed date; a changed reading requires the\nreview in step 4. When no image-view tool is available, acknowledge that limitation\nand keep a triggered date pending rather than claiming a visual check. Keep the\nevidence record outside the requested workbook schema.\n\n#### Branch B — Inclusive total, rounding, and a later final total\n\n**Trigger:** the payment block contains a pre-round total (possibly labeled as\ntax-inclusive), a signed rounding adjustment, and a later `TOTAL`, `NET TOTAL`,\n`NETT TOTAL`, or other final-payable line. Different amounts in these roles are\nnot conflicting OCR readings of one field.\n\n**Execute:**\n1. Crop and read the whole payment block together. Assign candidates to separate\n   roles: subtotal, tax\u002Fservice components, pre-round payable, signed adjustment,\n   final payable, tendered cash, and change. Keep a tax-summary table outside this\n   payment block; its own `TOTAL` is not the amount due.\n2. Preserve the `+` or `-` on the adjustment. Parse these monetary tokens with\n   `Decimal` and check `final_payable == pre_round_payable + signed_adjustment`.\n   The sign is part of the value, not punctuation to discard. If the pre-round\n   payable already includes tax\u002Fservice charges, use those components only to\n   cross-check it, not to add them again.\n3. Select the explicitly printed final-payable amount when the field association\n   and arithmetic agree, even if an earlier tax-inclusive amount occurs in more\n   OCR passes or has a higher-ranked keyword. The final label can be larger or\n   bolder than surrounding text; inspect that line separately if OCR misses it.\n4. If a digit or separator is missing or the arithmetic disagrees, review the\n   corresponding local crop. Accept the result only after resolving the reading;\n   do not invent a rounding adjustment or silently force the arithmetic to fit.\n\n**Confirm when:** the final amount is anchored to its own label and verified\nagainst the printed settlement relation. If no rounding\u002Ffinal-total pattern is\npresent, resolve the final-payable role from the receipt's actual labels; do not\napply this branch's adjustment speculatively. Keywords such as `GST` identify a\npossible component, not an automatic reason to discard an inclusive payable.\n\n#### Branch C — Amount split across lines or repaired digits\n\n**Trigger:** an amount keyword has no number on its line, or a candidate relies\non inserting a decimal, removing an OCR character, or joining separated digits.\n\n**Execute:** crop the label and neighboring lines together. Locate the numeric\nregion that is visually associated with that label; exclude quantity, date,\nidentifier, tax-rate, and change fields. Use next-line\u002Ftrailing tokens only as\ncandidates. Support each punctuation\u002Fdigit repair from the original monetary\nregion, and then apply Branch B if settlement roles are present.\n\n**Confirm when:** both the label association and repaired number are supported.\nNeither the last number on a line nor successful parsing clears this branch.\n\n#### Branch D — Reusing confirmed fields after a parser change\n\n**Trigger:** changing shared parsing\u002Fselection logic, adding a raw-OCR preference,\nor retrying extraction after any fields have been confirmed.\n\n**Execute:** save and reload the confirmed field values with their evidence before\nthe change. Run the new logic into provisional candidates. Merge only reviewed\nupdates to unresolved or demonstrably affected fields; retain every other\nconfirmed field. A changed candidate requires stronger same-field evidence and\na recorded reason before replacing a confirmed value. Rebuild the deliverable\nfrom this merged accepted state, not from fresh unreviewed batch output.\n\n**Confirm when:** a before\u002Fafter field comparison shows that every replacement\nhas been reviewed. Execute the write, reopen the destination workbook, and compare\nits stored values with the accepted state. Logs or values left only in memory do\nnot demonstrate delivery. Reserve budget for this final readback instead of\nunnecessarily repeating full-batch OCR at the end. Reserve at least the last\nfive available model\u002Ftool iterations for delivery: save the merged accepted\nstate, execute any still-unapplied targeted updates, reopen the actual output,\nand compare its stored fields, coverage, schema, and ordering with that state.\nCheck the remaining budget before starting another OCR experiment or group of\nimage views. Once the delivery reserve is reached, finish writing and readback\ninstead of starting optional rechecks. If required fields remain unresolved,\nsave the current work and report them rather than guessing or claiming completion.\n\n### Numeric Normalization for Receipts and Forms\n\nOCR frequently corrupts decimal separators and digit spacing in monetary amounts.\nApply the following repairs only inside a monetary region associated with its\nlabel, and only when its printed evidence supports the repair (Branch C):\n\n- Replace `:` between digits with `.` (e.g., `92:80` -> `92.80`).\n- Collapse a single space between a 1-4 digit group and a 2-digit group into `.` (e.g., `92 80` -> `92.80`, `1 234 56` -> keep the comma-thousands intact but treat trailing ` 56` as decimals only when no other decimal separator is present).\n- Reinterpret bare digit clusters of length >=3 whose last two digits look like cents as `X.YY` when no decimal separator was recognized (e.g., `9280` -> `92.80`) **only** when the line contains a strong keyword such as `GRAND TOTAL` and no better candidate exists.\n- Remove clearly extraneous surrounding OCR marks; retain decimal\u002Fthousands punctuation and any genuine sign, especially on rounding adjustments.\n- Accept commas as thousands separators: `1,234.56` -> `1234.56`.\n\nPrefer a well-formed monetary token in the identified field. Next-line candidates\nand decimal repairs must pass Branch C; this helper parses a token but does not\ndecide whether it is the final amount. Keep date and ID parsing separate.\n\n```python\nimport re\nfrom decimal import Decimal\n\nMONEY_STRICT = re.compile(r'[+-]?(?:\\d{1,3}(?:,\\d{3})+|\\d+)\\.\\d{2}')\n\ndef parse_money(token: str, *, decimal_repair_supported=False):\n    \"\"\"Parse a field-associated monetary token, preserving a printed sign.\"\"\"\n    if token is None:\n        return None\n    s = token.strip().replace('\\u2212', '-')\n    if MONEY_STRICT.fullmatch(s):\n        return Decimal(s.replace(',', ''))\n    if not decimal_repair_supported:\n        return None\n    # Enable only after the original region establishes the decimal position.\n    candidates = [\n        re.sub(r'(\\d):(\\d{2})$', r'\\1.\\2', s),\n        re.sub(r'(\\d)\\s+(\\d{2})$', r'\\1.\\2', s),\n    ]\n    if re.fullmatch(r'[+-]?\\d{3,}', s):\n        candidates.append(s[:-2] + '.' + s[-2:])\n    for candidate in candidates:\n        if MONEY_STRICT.fullmatch(candidate):\n            return Decimal(candidate.replace(',', ''))\n    return None\n```\n\n\n## Error Handling\n\n### Common Issues and Solutions\n\n**Issue**: Tesseract not found\n\n```python\n# Verify Tesseract is installed\ntry:\n    pytesseract.get_tesseract_version()\nexcept pytesseract.TesseractNotFoundError:\n    print(\"Tesseract is not installed or not in PATH\")\n```\n\n**Issue**: Poor OCR quality\n\n- Preprocess image (grayscale, contrast, sharpen)\n- Use appropriate PSM mode for the document type\n- Ensure image resolution is sufficient (300+ DPI)\n\n**Issue**: Empty or garbage output\n\n- Check if image contains actual text\n- Try different PSM modes\n- Verify image is not corrupted\n\n**Issue**: Keyword detected but no numeric value parsed\n\n- Do not immediately return null. First:\n  1. Run Branch C on the label and its neighboring numeric region.\n  2. If multiple settlement amounts or rounding appear, run Branch B.\n  3. Retain unresolved candidates and continue focused review within the available budget.\n- If the field remains unresolved, report extraction failure instead of accepting\n  an unrelated but parseable number. Non-empty ambiguous fields use the same\n  decision branches; review is not limited to null values.\n\n\n## Quality Self-Check\n\nBefore returning results, verify:\n\n- [ ] Output is valid JSON (use `json.loads()` to validate)\n- [ ] All required fields are present (`success`, `filename`, `extracted_text`, `confidence`, `metadata`)\n- [ ] Text preserves logical reading order\n- [ ] Confidence level reflects actual OCR quality\n- [ ] Warnings array includes all detected issues\n- [ ] Special characters are properly escaped in JSON\n\n### Structured-Value Extraction Self-Check\n\nWhen the caller extracts specific fields (amounts, dates, IDs), also verify:\n\n- [ ] For every image where a target keyword (e.g., `GRAND TOTAL`, `TOTAL RM`) is detected in **any** OCR variant, a candidate value was produced, or the reason for failure is explicitly logged.\n- [ ] Every unconfirmed date with `8` in a candidate day\u002Fmonth passed Branch A with an actual view; year-only `8` and standalone `3` did not trigger unnecessary viewing, while ambiguous or changed dates still received review.\n- [ ] Each accepted date correction was saved and read back before continuing the review queue; unchanged confirmed dates reused their recorded inspection.\n- [ ] Settlement totals passed Branch B where applicable; pre-round, rounded, tax-summary, tendered, and change amounts were kept in separate roles.\n- [ ] Decimal repairs and split-line candidates passed Branch C, preserving a genuine adjustment sign.\n- [ ] Parser changes and retries passed Branch D; confirmed fields were preserved unless an evidenced correction was accepted.\n- [ ] The delivery reserve was used to apply all accepted updates to the requested file, and readback matched the accepted fields and caller's schema, coverage, and ordering.\n\n\n## Limitations\n\n- Tesseract works best with printed text; handwriting recognition is limited\n- Accuracy decreases with decorative fonts, artistic text, or extreme stylization\n- Mathematical equations and special notation may not extract accurately\n- Redacted or watermarked text cannot be recovered\n- Severe image degradation (blur, noise, low resolution) reduces accuracy\n- Complex multi-column layouts may require custom PSM configuration\n- Thin punctuation (decimal points, colons) is often lost or substituted; always attempt numeric normalization before rejecting a value\n\n\n## Version History\n\n- **1.0.0** (2026-01-13): Initial release with Tesseract\u002Fpytesseract OCR\n- **1.1.0**: Added value-level voting across variants, numeric normalization guidance for OCR-corrupted amounts, and structured-value extraction self-check.\n",{"name":5,"description":7},"main","cdcb6d230f3b15c3c33f8a2bdd16439f43602cbe","evaluation\u002Fresults\u002F对于jpg-ocr-stat的尝试\u002Fskills\u002Fbundles\u002Ftest-skillaxe-date-round26-opus80-20261012\u002Finputs\u002Fround-26\u002Fskills\u002Fimage-ocr",1,"https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark","https:\u002F\u002Fgithub.com\u002Flinyouxue","2026-11-14 20:28:35",{"source":8,"skillName":5,"commands":32,"prompt":42,"zipAvailable":43},[33,36,39],{"tool":34,"command":35},"npx","npx skills add linyouxue\u002Fskill-repair-benchmark --skill image-ocr",{"tool":37,"command":38},"bunx","bunx skills add linyouxue\u002Fskill-repair-benchmark --skill image-ocr",{"tool":40,"command":41},"pnpm","pnpm dlx skills add linyouxue\u002Fskill-repair-benchmark --skill image-ocr","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fevaluation\u002Fresults\u002F对于jpg-ocr-stat的尝试\u002Fskills\u002Fbundles\u002Ftest-skillaxe-date-round26-opus80-20261012\u002Finputs\u002Fround-26\u002Fskills\u002Fimage-ocr\u002FSKILL.md",true,[],[46,53,60,66,71,76],{"skillNo":47,"name":48,"nameZh":6,"description":49,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":50,"categories":51,"keywords":52,"publishedAt":19},"01M7E1HGVRY73NBJ022CJG13X6","video-frame-extraction","Extract frames from video files and save them as images using OpenCV","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_03\u002Finputs\u002Fskills\u002Fvideo-frame-extraction\u002FSKILL.md",[],[],{"skillNo":54,"name":55,"nameZh":6,"description":56,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":57,"categories":58,"keywords":59,"publishedAt":19},"01M7E1HAG5R7CJ9C8NYJS771MB","openai-vision","Analyze images and multi-frame sequences using OpenAI GPT vision models","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_03\u002Finputs\u002Fskills\u002Fopenai-vision\u002FSKILL.md",[],[],{"skillNo":61,"name":5,"nameZh":6,"description":62,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":63,"categories":64,"keywords":65,"publishedAt":19},"01M7E1H7DADKB6QBWKC2KQ63SZ","Extract text content from images using Tesseract OCR via Python, including structured field extraction from receipts and forms","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_03\u002Finputs\u002Fskills\u002Fimage-ocr\u002FSKILL.md",[],[],{"skillNo":67,"name":48,"nameZh":6,"description":49,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":68,"categories":69,"keywords":70,"publishedAt":19},"01M7E1H0WBNT43TENGQY7AVZHD","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_02\u002Finputs\u002Fskills\u002Fvideo-frame-extraction\u002FSKILL.md",[],[],{"skillNo":72,"name":55,"nameZh":6,"description":56,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":73,"categories":74,"keywords":75,"publishedAt":19},"01M7E1GTC7D4N4Q2EF03875J9E","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_02\u002Finputs\u002Fskills\u002Fopenai-vision\u002FSKILL.md",[],[],{"skillNo":77,"name":5,"nameZh":6,"description":62,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":78,"categories":79,"keywords":80,"publishedAt":19},"01M7E1GQ92CWFMGH53KSSE4WER","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_02\u002Finputs\u002Fskills\u002Fimage-ocr\u002FSKILL.md",[],[]]