Last active
May 9, 2022 09:49
-
-
Save markscottwright/6034a55715c4a87bb563a8e3b9da33eb to your computer and use it in GitHub Desktop.
A single-file (although depending on Tesseract) tool for extracting text from images.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package scratch; | |
import java.awt.AWTException; | |
import java.awt.Color; | |
import java.awt.Dimension; | |
import java.awt.Font; | |
import java.awt.Frame; | |
import java.awt.Graphics; | |
import java.awt.Graphics2D; | |
import java.awt.GraphicsEnvironment; | |
import java.awt.Point; | |
import java.awt.Rectangle; | |
import java.awt.RenderingHints; | |
import java.awt.Robot; | |
import java.awt.Toolkit; | |
import java.awt.Transparency; | |
import java.awt.datatransfer.Clipboard; | |
import java.awt.datatransfer.DataFlavor; | |
import java.awt.datatransfer.Transferable; | |
import java.awt.datatransfer.UnsupportedFlavorException; | |
import java.awt.dnd.DnDConstants; | |
import java.awt.dnd.DropTarget; | |
import java.awt.dnd.DropTargetDropEvent; | |
import java.awt.event.ActionEvent; | |
import java.awt.event.KeyEvent; | |
import java.awt.event.MouseAdapter; | |
import java.awt.event.MouseEvent; | |
import java.awt.event.MouseMotionAdapter; | |
import java.awt.image.BufferedImage; | |
import java.io.File; | |
import java.io.IOException; | |
import java.util.List; | |
import java.util.Optional; | |
import javax.imageio.ImageIO; | |
import javax.swing.AbstractAction; | |
import javax.swing.JFrame; | |
import javax.swing.JPanel; | |
import javax.swing.JScrollPane; | |
import javax.swing.JSplitPane; | |
import javax.swing.JTextArea; | |
import javax.swing.KeyStroke; | |
import javax.swing.SwingUtilities; | |
import javax.swing.UnsupportedLookAndFeelException; | |
import net.sourceforge.tess4j.Tesseract; | |
import net.sourceforge.tess4j.TesseractException; | |
/** | |
* Needs this dependency: | |
* {@code<dependency><groupId>net.sourceforge.tess4j</groupId><artifactId>tess4j</artifactId><version>5.2.1</version></dependency>} | |
* | |
* <br/> | |
* <br/> | |
* | |
* And needs this to be checked out: git clone | |
* https://github.com/tesseract-ocr/tessdata_best | |
*/ | |
@SuppressWarnings("serial") | |
public class ImageOcr extends JFrame { | |
public class ImageControl extends JPanel { | |
private BufferedImage zoomedImage = null; | |
private BufferedImage originalImage = null; | |
private double zoom = 1.0; | |
private Point selectionOrigin = null; | |
private Point selectionCorner = null; | |
public ImageControl(BufferedImage image) { | |
this.originalImage = this.zoomedImage = image; | |
addListeners(); | |
} | |
public ImageControl() { | |
addListeners(); | |
} | |
private void addListeners() { | |
// zoom on mouse wheel | |
addMouseWheelListener(e -> { | |
if (e.isControlDown()) { | |
zoom = Math.max(.2, zoom + .05 * -e.getWheelRotation()); | |
this.zoomedImage = resize(originalImage, (int) (originalImage.getWidth() * zoom)); | |
selectionOrigin = selectionCorner = null; | |
revalidate(); | |
} else { | |
getParent().dispatchEvent(e); | |
} | |
}); | |
// start a clipping rectangle | |
addMouseListener(new MouseAdapter() { | |
@Override | |
public void mousePressed(MouseEvent e) { | |
selectionOrigin = e.getPoint(); | |
selectionCorner = null; | |
requestFocus(); | |
} | |
@Override | |
public void mouseReleased(MouseEvent e) { | |
getSelection().ifPresent(i -> onNewSelection(i)); | |
} | |
}); | |
addMouseMotionListener(new MouseMotionAdapter() { | |
@Override | |
public void mouseDragged(MouseEvent e) { | |
selectionCorner = e.getPoint(); | |
repaint(); | |
} | |
}); | |
// paste any image from the clipboard | |
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_V, KeyEvent.CTRL_DOWN_MASK), "ctrlV"); | |
getActionMap().put("ctrlV", new AbstractAction() { | |
@Override | |
public void actionPerformed(ActionEvent e) { | |
Optional<BufferedImage> imageInClipboard = getImageInClipboard(); | |
imageInClipboard.ifPresent(i -> { | |
setImage(i); | |
}); | |
} | |
}); | |
// capture a screenshot, hiding this window first | |
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK), | |
"ctrlShiftS"); | |
getActionMap().put("ctrlShiftS", new AbstractAction() { | |
@Override | |
public void actionPerformed(ActionEvent e) { | |
JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(ImageControl.this); | |
if (frame == null) | |
return; | |
int originalWindowState = frame.getState(); | |
frame.setState(Frame.ICONIFIED); | |
SwingUtilities.invokeLater(() -> { | |
try { | |
Thread.sleep(50); | |
Rectangle screenRectangle = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()); | |
BufferedImage screenCapture = new Robot().createScreenCapture(screenRectangle); | |
setImage(screenCapture); | |
frame.setState(originalWindowState); | |
} catch (AWTException | InterruptedException e1) { | |
e1.printStackTrace(); | |
} | |
getRootPane().setVisible(true); | |
}); | |
} | |
}); | |
// allow files or images to be dragged onto this control | |
setDropTarget(new DropTarget() { | |
@Override | |
public synchronized void drop(DropTargetDropEvent e) { | |
try { | |
if (e.getTransferable().isDataFlavorSupported(DataFlavor.imageFlavor)) { | |
e.acceptDrop(DnDConstants.ACTION_COPY); | |
Object transferData = e.getTransferable().getTransferData(DataFlavor.imageFlavor); | |
if (transferData != null) { | |
setImage((BufferedImage) transferData); | |
} | |
} else if (e.getTransferable().isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { | |
e.acceptDrop(DnDConstants.ACTION_COPY); | |
Object transferData = e.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); | |
@SuppressWarnings("unchecked") | |
List<File> files = (List<File>) transferData; | |
if (files.size() > 0) { | |
setImage(ImageIO.read(files.get(0))); | |
} | |
} | |
} catch (UnsupportedFlavorException | IOException e1) { | |
} | |
} | |
}); | |
} | |
/** | |
* Takes a BufferedImage and resizes it according to the provided targetSize | |
* | |
* @param src the source BufferedImage | |
* @param targetSize maximum height (if portrait) or width (if landscape) | |
* @return a resized version of the provided BufferedImage | |
*/ | |
private BufferedImage resize(BufferedImage src, int targetSize) { | |
if (targetSize <= 0) { | |
return src; // this can't be resized | |
} | |
int targetWidth = targetSize; | |
int targetHeight = targetSize; | |
float ratio = ((float) src.getHeight() / (float) src.getWidth()); | |
if (ratio <= 1) { // square or landscape-oriented image | |
targetHeight = (int) Math.ceil((float) targetWidth * ratio); | |
} else { // portrait image | |
targetWidth = Math.round((float) targetHeight / ratio); | |
} | |
BufferedImage bi = new BufferedImage(targetWidth, targetHeight, | |
src.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB | |
: BufferedImage.TYPE_INT_ARGB); | |
Graphics2D g2d = bi.createGraphics(); | |
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); // produces | |
g2d.drawImage(src, 0, 0, targetWidth, targetHeight, null); | |
g2d.dispose(); | |
return bi; | |
} | |
@Override | |
protected void paintComponent(Graphics g) { | |
super.paintComponent(g); | |
if (zoomedImage != null) { | |
g.drawImage(zoomedImage, 0, 0, zoomedImage.getWidth(), zoomedImage.getHeight(), null); | |
if (selectionCorner != null && selectionOrigin != null) { | |
int left = Math.min(selectionOrigin.x, selectionCorner.x); | |
int top = Math.min(selectionOrigin.y, selectionCorner.y); | |
g.setColor(Color.RED); | |
g.drawRect(left, top, Math.abs(selectionCorner.x - selectionOrigin.x), | |
Math.abs(selectionCorner.y - selectionOrigin.y)); | |
} | |
} | |
} | |
@Override | |
public Dimension getPreferredSize() { | |
if (zoomedImage != null) | |
return new Dimension(zoomedImage.getWidth(), zoomedImage.getHeight()); | |
else | |
return new Dimension(5, 5); | |
} | |
protected void onNewSelection(BufferedImage selection) { | |
} | |
private Optional<BufferedImage> getSelection() { | |
if (zoomedImage == null || selectionOrigin == null || selectionCorner == null | |
|| selectionOrigin.equals(selectionCorner)) | |
return Optional.empty(); | |
int left = Math.max(0, Math.min(selectionOrigin.x, selectionCorner.x)); | |
int top = Math.max(0, Math.min(selectionOrigin.y, selectionCorner.y)); | |
int width = Math.abs(selectionCorner.x - selectionOrigin.x); | |
int height = Math.abs(selectionCorner.y - selectionOrigin.y); | |
if (width == 0 || height == 0) | |
return Optional.empty(); | |
var selectedImage = zoomedImage.getSubimage(left, top, Math.min(width, zoomedImage.getWidth() - left), | |
Math.min(height, zoomedImage.getHeight() - top)); | |
return Optional.of(selectedImage); | |
} | |
public void setImage(BufferedImage i) { | |
originalImage = zoomedImage = i; | |
selectionOrigin = selectionCorner = null; | |
zoom = 1.0; | |
repaint(); | |
invalidate(); | |
} | |
} | |
public class OcrImageControl extends ImageControl { | |
private JTextArea ocrText; | |
public OcrImageControl(BufferedImage image, JTextArea ocrText) { | |
super(image); | |
this.ocrText = ocrText; | |
} | |
public OcrImageControl(JTextArea ocrText) { | |
this.ocrText = ocrText; | |
} | |
@Override | |
protected void onNewSelection(BufferedImage selection) { | |
Tesseract tesseract = new Tesseract(); | |
tesseract.setDatapath("tessdata_best"); | |
tesseract.setLanguage("eng"); | |
try { | |
String text = tesseract.doOCR(selection); | |
System.out.println(text); | |
ocrText.setText(text); | |
} catch (TesseractException e) { | |
e.printStackTrace(); | |
} | |
} | |
} | |
public ImageOcr() { | |
setTitle("Image OCR"); | |
JTextArea ocrText = new JTextArea(); | |
ocrText.setFont(new Font("Arial", Font.PLAIN, 24)); | |
Optional<BufferedImage> maybeImageInClipboard = getImageInClipboard(); | |
ImageControl imageControl = maybeImageInClipboard.isPresent() | |
? new OcrImageControl(maybeImageInClipboard.get(), ocrText) | |
: new OcrImageControl(ocrText); | |
JSplitPane splitter = new JSplitPane(JSplitPane.VERTICAL_SPLIT, new JScrollPane(imageControl), | |
new JScrollPane(ocrText)); | |
splitter.setResizeWeight(1); | |
getContentPane().add(splitter); | |
SwingUtilities.invokeLater(() -> { | |
try { | |
Thread.sleep(50); | |
} catch (InterruptedException e) { | |
} | |
splitter.setDividerLocation(.9); | |
}); | |
} | |
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, | |
IllegalAccessException, UnsupportedLookAndFeelException { | |
for (String name : GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()) { | |
System.out.println(name); | |
} | |
ImageOcr mainWindow = new ImageOcr(); | |
mainWindow.setSize(800, 800); | |
mainWindow.setDefaultCloseOperation(EXIT_ON_CLOSE); | |
mainWindow.setVisible(true); | |
} | |
public static Optional<BufferedImage> getImageInClipboard() { | |
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); | |
Transferable contents = clipboard.getContents(null); | |
if (contents != null && contents.isDataFlavorSupported(DataFlavor.imageFlavor)) { | |
try { | |
Object transferDataObject = contents.getTransferData(DataFlavor.imageFlavor); | |
return Optional.of((BufferedImage) transferDataObject); | |
} catch (UnsupportedFlavorException | IOException e1) { | |
return Optional.empty(); | |
} | |
} | |
return Optional.empty(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment