Skip to content

Instantly share code, notes, and snippets.

@markscottwright
Last active May 9, 2022 09:49
Show Gist options
  • Save markscottwright/6034a55715c4a87bb563a8e3b9da33eb to your computer and use it in GitHub Desktop.
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.
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