A tool for WhatsApp Web that enables image printing and AI-powered text recognition (OCR).
// ==UserScript==
// @name WhatsApp Web Print & OCR
// @name:cs WhatsApp Web Tisk a OCR
// @namespace http://tampermonkey.net/
// @version 2.2
// @description A tool for WhatsApp Web that enables image printing and AI-powered text recognition (OCR).
// @description:cs Nástroj pro WhatsApp Web umožňující tisk obrázků a rozpoznávání textu (OCR) pomocí AI.
// @author Michal Dobiášovský
// @license MIT
// @match https://web.whatsapp.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @connect generativelanguage.googleapis.com
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// 1. Localization and Settings
const lang = (navigator.language || navigator.userLanguage).startsWith('cs') || (navigator.language || navigator.userLanguage).startsWith('sk') ? 'cs' : 'en';
const i18n = {
cs: {
apiKeyPrompt: 'Vložte Gemini API klíč (získáte na aistudio.google.com):',
analyzing: 'Analyzuji... ⏳',
noText: 'Text nenalezen.',
apiError: 'Chyba API.',
copyBtn: 'Zkopírovat',
printBtn: 'Tisk výběru',
closeBtn: 'Zavřít',
copiedAlert: 'Zkopírováno!',
menuSetKey: '🔑 Nastavit API klíč',
menuTogglePrint: '🖨️ Tisk obrázků: ',
menuToggleOCR: '🔍 AI OCR (Text): ',
on: 'ZAPNUTO',
off: 'VYPNUTO',
aiPrompt: 'EXTRACT TEXT ONLY. NO INTRO. NO CONVERSATION. Output only raw text.'
},
en: {
apiKeyPrompt: 'Enter Gemini API key (get it at aistudio.google.com):',
analyzing: 'Analyzing... ⏳',
noText: 'No text found.',
apiError: 'API Error.',
copyBtn: 'Copy Text',
printBtn: 'Print Selection',
closeBtn: 'Close',
copiedAlert: 'Copied!',
menuSetKey: '🔑 Set API Key',
menuTogglePrint: '🖨️ Image Print: ',
menuToggleOCR: '🔍 AI OCR (Text): ',
on: 'ON',
off: 'OFF',
aiPrompt: 'EXTRACT TEXT ONLY. NO INTRO. NO CONVERSATION. Output only raw text.'
}
};
const t = (key) => i18n[lang][key];
let isPrintEnabled = GM_getValue('SETTING_PRINT', true);
let isOcrEnabled = GM_getValue('SETTING_OCR', true);
// 2. Menu Commands
GM_registerMenuCommand(t('menuSetKey'), () => {
const key = prompt(t('apiKeyPrompt'), GM_getValue('GEMINI_API_KEY', ''));
if (key !== null) GM_setValue('GEMINI_API_KEY', key.trim());
});
GM_registerMenuCommand(t('menuTogglePrint') + (isPrintEnabled ? t('on') : t('off')), () => {
GM_setValue('SETTING_PRINT', !isPrintEnabled);
location.reload();
});
GM_registerMenuCommand(t('menuToggleOCR') + (isOcrEnabled ? t('on') : t('off')), () => {
GM_setValue('SETTING_OCR', !isOcrEnabled);
location.reload();
});
// 3. Helper: Geometric Detection (Highest immunity)
function getActivePreviewImage() {
const images = document.querySelectorAll('img[src^="blob:"]');
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
for (const img of images) {
const rect = img.getBoundingClientRect();
// Logic:
// 1. Image must be visible
// 2. Image must be larger than 250px (skip chat bubbles)
// 3. Image must cover the center of the screen (the main preview always does)
if (img.offsetParent !== null && rect.width > 250 && rect.height > 250) {
if (rect.left < centerX && rect.right > centerX && rect.top < centerY && rect.bottom > centerY) {
return img;
}
}
}
return null;
}
// 4. UI Elements & Selection CSS Fix
const selectionStyle = document.createElement('style');
selectionStyle.innerHTML = `
#wa-ocr-res-text {
user-select: text !important;
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
cursor: text !important;
}
#wa-ocr-modal * {
pointer-events: auto !important;
}
`;
document.head.appendChild(selectionStyle);
const printFab = document.createElement('div');
printFab.id = 'wa-print-fab';
printFab.style.cssText = 'position:fixed; bottom:30px; right:30px; z-index:10000; background-color:#00a884; color:white; width:60px; height:60px; border-radius:50%; display:none; justify-content:center; align-items:center; cursor:pointer; box-shadow:0 4px 15px rgba(0,0,0,0.4); border:2px solid white;';
printFab.innerHTML = `<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>`;
const ocrFab = document.createElement('div');
ocrFab.id = 'wa-ocr-fab';
ocrFab.style.cssText = 'position:fixed; bottom:100px; right:30px; z-index:10000; background-color:#1a73e8; color:white; width:60px; height:60px; border-radius:50%; display:none; justify-content:center; align-items:center; cursor:pointer; box-shadow:0 4px 15px rgba(0,0,0,0.4); border:2px solid white; font-weight:bold; font-size:22px; font-family:sans-serif;';
ocrFab.innerText = 'T';
const modal = document.createElement('div');
modal.id = 'wa-ocr-modal';
modal.style.cssText = 'position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:20000; display:none; justify-content:center; align-items:center;';
modal.innerHTML = `
<div style="background:white; width:85%; max-width:700px; padding:25px; border-radius:12px; display:flex; flex-direction:column; gap:15px;">
<div id="wa-ocr-res-text" style="width:100%; height:300px; border:1px solid #ccc; border-radius:6px; padding:12px; font-family:sans-serif; overflow-y:auto; white-space:pre-wrap; background:#fff; color:#111; font-size:14px;"></div>
<div style="display:flex; gap:10px; justify-content:flex-end;">
<button id="wa-ocr-print-text" style="padding:10px 18px; border-radius:6px; cursor:pointer; border:none; font-weight:bold; background:#555; color:white;">${t('printBtn')}</button>
<button id="wa-ocr-copy" style="padding:10px 18px; border-radius:6px; cursor:pointer; border:none; font-weight:bold; background:#00a884; color:white;">${t('copyBtn')}</button>
<button id="wa-ocr-close" style="padding:10px 18px; border-radius:6px; cursor:pointer; border:none; font-weight:bold; background:#eee; color:#333;">${t('closeBtn')}</button>
</div>
</div>
`;
// 5. Core Logic
async function getBase64(url) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET", url: url, responseType: "blob",
onload: (res) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(res.response);
}
});
});
}
async function handlePrint(url) {
const dataUrl = await getBase64(url);
const pWin = window.open('', '_blank');
pWin.document.write(`<html><head><title> </title><style>@page{margin:0;} body{margin:0;display:grid;place-items:center;height:100vh;} img{max-width:94%;max-height:94%;object-fit:contain;}</style></head><body><img src="${dataUrl}"></body></html>`);
pWin.document.close();
setTimeout(() => { pWin.print(); pWin.close(); }, 500);
}
async function handleOCR(url) {
let key = GM_getValue('GEMINI_API_KEY');
if (!key) {
key = prompt(t('apiKeyPrompt'));
if (!key) return;
GM_setValue('GEMINI_API_KEY', key.trim());
}
modal.style.display = 'flex';
const area = document.getElementById('wa-ocr-res-text');
area.innerText = t('analyzing');
const fullBase64 = await getBase64(url);
GM_xmlhttpRequest({
method: "POST",
url: `https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent?key=${key}`,
headers: { "Content-Type": "application/json" },
data: JSON.stringify({
contents: [{ parts: [{ text: t('aiPrompt') }, { inline_data: { mime_type: "image/jpeg", data: fullBase64.split(',')[1] } }] }]
}),
onload: (res) => {
const json = JSON.parse(res.responseText);
let result = json.candidates?.[0]?.content?.parts?.[0]?.text || t('noText');
area.innerText = result.replace(/^(Sure|Here is|Extracted):?\s*/i, "").trim();
},
onerror: () => area.innerText = t('apiError')
});
}
// 6. Events
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.style.display === 'flex') {
e.preventDefault(); e.stopImmediatePropagation();
modal.style.display = 'none';
} else if (isPrintEnabled && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'p') {
const currentImg = getActivePreviewImage();
if (currentImg) { e.preventDefault(); e.stopImmediatePropagation(); handlePrint(currentImg.src); }
}
}, true);
setInterval(() => {
if (!document.body) return;
if (!document.getElementById('wa-print-fab')) {
document.body.append(printFab, ocrFab, modal);
document.getElementById('wa-ocr-close').onclick = () => modal.style.display = 'none';
document.getElementById('wa-ocr-copy').onclick = () => {
const text = document.getElementById('wa-ocr-res-text').innerText;
navigator.clipboard.writeText(text);
alert(t('copiedAlert'));
};
document.getElementById('wa-ocr-print-text').onclick = () => {
const selection = window.getSelection().toString();
const fullText = document.getElementById('wa-ocr-res-text').innerText;
const textToPrint = (selection && selection.trim().length > 0) ? selection : fullText;
const pWin = window.open('', '_blank');
pWin.document.write(`<html><head><title> </title><style>@page{margin:15mm;} body{font-family:sans-serif; padding:10px;} pre{white-space:pre-wrap; font-size:14px;}</style></head><body><pre>${textToPrint}</pre></body></html>`);
pWin.document.close();
setTimeout(() => { pWin.print(); pWin.close(); }, 500);
};
}
const currentImg = getActivePreviewImage();
const isVisible = currentImg !== null;
if (isVisible) {
if (isPrintEnabled) printFab.style.display = 'flex';
if (isOcrEnabled) ocrFab.style.display = 'flex';
printFab.onclick = () => handlePrint(currentImg.src);
ocrFab.onclick = () => handleOCR(currentImg.src);
} else {
printFab.style.display = 'none';
ocrFab.style.display = 'none';
if (modal.style.display === 'flex') modal.style.display = 'none';
}
}, 500);
})();// ==UserScript==
// @name WhatsApp Web Print & OCR
// @name:cs WhatsApp Web Tisk a OCR
// @namespace http://tampermonkey.net/
// @version 2.1
// @description A tool for WhatsApp Web that enables image printing and AI-powered text recognition (OCR).
// @description:cs Nástroj pro WhatsApp Web umožňující tisk obrázků a rozpoznávání textu (OCR) pomocí AI.
// @author Michal Dobiášovský
// @license MIT
// @match https://web.whatsapp.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @connect generativelanguage.googleapis.com
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// 1. Localization and Settings
const lang = (navigator.language || navigator.userLanguage).startsWith('cs') || (navigator.language || navigator.userLanguage).startsWith('sk') ? 'cs' : 'en';
const i18n = {
cs: {
apiKeyPrompt: 'Vložte Gemini API klíč (získáte na aistudio.google.com):',
analyzing: 'Analyzuji... ⏳',
noText: 'Text nenalezen.',
apiError: 'Chyba API.',
copyBtn: 'Zkopírovat',
printBtn: 'Tisk výběru',
closeBtn: 'Zavřít',
copiedAlert: 'Zkopírováno!',
menuSetKey: '🔑 Nastavit API klíč',
menuTogglePrint: '🖨️ Tisk obrázků: ',
menuToggleOCR: '🔍 AI OCR (Text): ',
on: 'ZAPNUTO',
off: 'VYPNUTO',
aiPrompt: 'EXTRACT TEXT ONLY. NO INTRO. NO CONVERSATION. Output only raw text.'
},
en: {
apiKeyPrompt: 'Enter Gemini API key (get it at aistudio.google.com):',
analyzing: 'Analyzing... ⏳',
noText: 'No text found.',
apiError: 'API Error.',
copyBtn: 'Copy Text',
printBtn: 'Print Selection',
closeBtn: 'Close',
copiedAlert: 'Copied!',
menuSetKey: '🔑 Set API Key',
menuTogglePrint: '🖨️ Image Print: ',
menuToggleOCR: '🔍 AI OCR (Text): ',
on: 'ON',
off: 'OFF',
aiPrompt: 'EXTRACT TEXT ONLY. NO INTRO. NO CONVERSATION. Output only raw text.'
}
};
const t = (key) => i18n[lang][key];
let isPrintEnabled = GM_getValue('SETTING_PRINT', true);
let isOcrEnabled = GM_getValue('SETTING_OCR', true);
// 2. Menu Commands
GM_registerMenuCommand(t('menuSetKey'), () => {
const key = prompt(t('apiKeyPrompt'), GM_getValue('GEMINI_API_KEY', ''));
if (key !== null) GM_setValue('GEMINI_API_KEY', key.trim());
});
GM_registerMenuCommand(t('menuTogglePrint') + (isPrintEnabled ? t('on') : t('off')), () => {
GM_setValue('SETTING_PRINT', !isPrintEnabled);
location.reload();
});
GM_registerMenuCommand(t('menuToggleOCR') + (isOcrEnabled ? t('on') : t('off')), () => {
GM_setValue('SETTING_OCR', !isOcrEnabled);
location.reload();
});
// 3. UI Elements & Selection CSS Fix
const selectionStyle = document.createElement('style');
selectionStyle.innerHTML = `
#wa-ocr-res-text {
user-select: text !important;
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
cursor: text !important;
}
#wa-ocr-modal * {
pointer-events: auto !important;
}
`;
document.head.appendChild(selectionStyle);
const printFab = document.createElement('div');
printFab.id = 'wa-print-fab';
printFab.style.cssText = 'position:fixed; bottom:30px; right:30px; z-index:10000; background-color:#00a884; color:white; width:60px; height:60px; border-radius:50%; display:none; justify-content:center; align-items:center; cursor:pointer; box-shadow:0 4px 15px rgba(0,0,0,0.4); border:2px solid white;';
printFab.innerHTML = `<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>`;
const ocrFab = document.createElement('div');
ocrFab.id = 'wa-ocr-fab';
ocrFab.style.cssText = 'position:fixed; bottom:100px; right:30px; z-index:10000; background-color:#1a73e8; color:white; width:60px; height:60px; border-radius:50%; display:none; justify-content:center; align-items:center; cursor:pointer; box-shadow:0 4px 15px rgba(0,0,0,0.4); border:2px solid white; font-weight:bold; font-size:22px; font-family:sans-serif;';
ocrFab.innerText = 'T';
const modal = document.createElement('div');
modal.id = 'wa-ocr-modal';
modal.style.cssText = 'position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:20000; display:none; justify-content:center; align-items:center;';
modal.innerHTML = `
<div style="background:white; width:85%; max-width:700px; padding:25px; border-radius:12px; display:flex; flex-direction:column; gap:15px;">
<div id="wa-ocr-res-text" style="width:100%; height:300px; border:1px solid #ccc; border-radius:6px; padding:12px; font-family:sans-serif; overflow-y:auto; white-space:pre-wrap; background:#fff; color:#111; font-size:14px;"></div>
<div style="display:flex; gap:10px; justify-content:flex-end;">
<button id="wa-ocr-print-text" style="padding:10px 18px; border-radius:6px; cursor:pointer; border:none; font-weight:bold; background:#555; color:white;">${t('printBtn')}</button>
<button id="wa-ocr-copy" style="padding:10px 18px; border-radius:6px; cursor:pointer; border:none; font-weight:bold; background:#00a884; color:white;">${t('copyBtn')}</button>
<button id="wa-ocr-close" style="padding:10px 18px; border-radius:6px; cursor:pointer; border:none; font-weight:bold; background:#eee; color:#333;">${t('closeBtn')}</button>
</div>
</div>
`;
// 4. Core Logic
async function getBase64(url) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET", url: url, responseType: "blob",
onload: (res) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(res.response);
}
});
});
}
async function handlePrint(url) {
const dataUrl = await getBase64(url);
const pWin = window.open('', '_blank');
pWin.document.write(`<html><head><title> </title><style>@page{margin:0;} body{margin:0;display:grid;place-items:center;height:100vh;} img{max-width:94%;max-height:94%;object-fit:contain;}</style></head><body><img src="${dataUrl}"></body></html>`);
pWin.document.close();
setTimeout(() => { pWin.print(); pWin.close(); }, 500);
}
async function handleOCR(url) {
let key = GM_getValue('GEMINI_API_KEY');
if (!key) {
key = prompt(t('apiKeyPrompt'));
if (!key) return;
GM_setValue('GEMINI_API_KEY', key.trim());
}
modal.style.display = 'flex';
const area = document.getElementById('wa-ocr-res-text');
area.innerText = t('analyzing');
const fullBase64 = await getBase64(url);
GM_xmlhttpRequest({
method: "POST",
url: `https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent?key=${key}`,
headers: { "Content-Type": "application/json" },
data: JSON.stringify({
contents: [{ parts: [{ text: t('aiPrompt') }, { inline_data: { mime_type: "image/jpeg", data: fullBase64.split(',')[1] } }] }]
}),
onload: (res) => {
const json = JSON.parse(res.responseText);
let result = json.candidates?.[0]?.content?.parts?.[0]?.text || t('noText');
area.innerText = result.replace(/^(Sure|Here is|Extracted):?\s*/i, "").trim();
},
onerror: () => area.innerText = t('apiError')
});
}
// 5. Events
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.style.display === 'flex') {
e.preventDefault(); e.stopImmediatePropagation();
modal.style.display = 'none';
} else if (isPrintEnabled && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'p') {
const img = document.querySelector('img._ao3e[src^="blob:"]');
if (img && img.offsetParent) { e.preventDefault(); e.stopImmediatePropagation(); handlePrint(img.src); }
}
}, true);
setInterval(() => {
if (!document.body) return;
// Prevent Interval from disturbing the DOM if modal is active
if (!document.getElementById('wa-print-fab')) {
document.body.append(printFab, ocrFab, modal);
document.getElementById('wa-ocr-close').onclick = () => modal.style.display = 'none';
document.getElementById('wa-ocr-copy').onclick = () => {
const text = document.getElementById('wa-ocr-res-text').innerText;
navigator.clipboard.writeText(text);
alert(t('copiedAlert'));
};
document.getElementById('wa-ocr-print-text').onclick = () => {
const selection = window.getSelection().toString();
const fullText = document.getElementById('wa-ocr-res-text').innerText;
const textToPrint = (selection && selection.trim().length > 0) ? selection : fullText;
const pWin = window.open('', '_blank');
pWin.document.write(`<html><head><title> </title><style>@page{margin:15mm;} body{font-family:sans-serif; padding:10px;} pre{white-space:pre-wrap; font-size:14px;}</style></head><body><pre>${textToPrint}</pre></body></html>`);
pWin.document.close();
setTimeout(() => { pWin.print(); pWin.close(); }, 500);
};
}
const img = document.querySelector('img._ao3e[src^="blob:"]');
const isVisible = img && img.offsetParent;
// Only toggle FAB visibility, don't re-append to avoid focus loss
if (isVisible) {
if (isPrintEnabled) printFab.style.display = 'flex';
if (isOcrEnabled) ocrFab.style.display = 'flex';
printFab.onclick = () => handlePrint(img.src);
ocrFab.onclick = () => handleOCR(img.src);
} else {
printFab.style.display = 'none';
ocrFab.style.display = 'none';
if (modal.style.display === 'flex') modal.style.display = 'none';
}
}, 500);
})();