All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nu.pattern.OpenCV Maven / Gradle / Ivy

There is a newer version: 4.9.0-0
Show newest version
package nu.pattern;

import org.opencv.core.Core;
import sun.reflect.CallerSensitive;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

public class OpenCV {

  private final static Logger logger = Logger.getLogger(OpenCV.class.getName());

  static enum OS {
    OSX("^[Mm]ac OS X$"),
    LINUX("^[Ll]inux$"),
    WINDOWS("^[Ww]indows.*");

    private final Set patterns;

    private OS(final String... patterns) {
      this.patterns = new HashSet();

      for (final String pattern : patterns) {
        this.patterns.add(Pattern.compile(pattern));
      }
    }

    private boolean is(final String id) {
      for (final Pattern pattern : patterns) {
        if (pattern.matcher(id).matches()) {
          return true;
        }
      }
      return false;
    }

    public static OS getCurrent() {
      final String osName = System.getProperty("os.name");

      for (final OS os : OS.values()) {
        if (os.is(osName)) {
          logger.log(Level.FINEST, "Current environment matches operating system descriptor \"{0}\".", os);
          return os;
        }
      }

      throw new UnsupportedOperationException(String.format("Operating system \"%s\" is not supported.", osName));
    }
  }

  static enum Arch {
    X86_32("i386", "i686"),
    X86_64("amd64", "x86_64");

    private final Set patterns;

    private Arch(final String... patterns) {
      this.patterns = new HashSet(Arrays.asList(patterns));
    }

    private boolean is(final String id) {
      return patterns.contains(id);
    }

    public static Arch getCurrent() {
      final String osArch = System.getProperty("os.arch");

      for (final Arch arch : Arch.values()) {
        if (arch.is(osArch)) {
          logger.log(Level.FINEST, "Current environment matches architecture descriptor \"{0}\".", arch);
          return arch;
        }
      }

      throw new UnsupportedOperationException(String.format("Architecture \"%s\" is not supported.", osArch));
    }
  }

  private static class UnsupportedPlatformException extends RuntimeException {
    private UnsupportedPlatformException(final OS os, final Arch arch) {
      super(String.format("Operating system \"%s\" and architecture \"%s\" are not supported.", os, arch));
    }
  }

  private static class TemporaryDirectory {
    final Path path;

    public TemporaryDirectory() {
      try {
        path = Files.createTempDirectory("");
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    public Path getPath() {
      return path;
    }

    public TemporaryDirectory markDeleteOnExit() {
      Runtime.getRuntime().addShutdownHook(new Thread() {
        @Override
        public void run() {
          delete();
        }
      });

      return this;
    }

    public void delete() {
      if (!Files.exists(path)) {
        return;
      }

      try {
        Files.walkFileTree(path, new SimpleFileVisitor() {
          @Override
          public FileVisitResult postVisitDirectory(final Path dir, final IOException e)
              throws IOException {
            Files.deleteIfExists(dir);
            return super.postVisitDirectory(dir, e);
          }

          @Override
          public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs)
              throws IOException {
            Files.deleteIfExists(file);
            return super.visitFile(file, attrs);
          }
        });
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

  }

  /**
   * Exactly once per {@link ClassLoader}, attempt to load the native library (via {@link System#loadLibrary(String)} with {@link Core#NATIVE_LIBRARY_NAME}). If the first attempt fails, the native binary will be extracted from the classpath to a temporary location (which gets cleaned up on shutdown), that location is added to the {@code java.library.path} system property and {@link ClassLoader#usr_paths}, and then another call to load the library is made. Note this method uses reflection to gain access to private memory in {@link ClassLoader} as there's no documented method to augment the library path at runtime. Spurious calls are safe.
   */
  public static void loadShared() {
    SharedLoader.getInstance();
  }

  /**
   * @see Initialization-on-demand holder idiom
   */
  private static class SharedLoader {
    private Path libraryPath;

    private SharedLoader() {
      try {
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        logger.log(Level.FINEST, "Loaded existing OpenCV library \"{0}\" from library path.", Core.NATIVE_LIBRARY_NAME);
      } catch (final UnsatisfiedLinkError ule) {

        /* Only update the library path and load if the original error indicates it's missing from the library path. */
        if (!String.format("no %s in java.library.path", Core.NATIVE_LIBRARY_NAME).equals(ule.getMessage())) {
          logger.log(Level.FINEST, String.format("Encountered unexpected loading error."), ule);
          throw ule;
        }

        /* Retain this path for cleaning up the library path later. */
        this.libraryPath = extractNativeBinary();

        addLibraryPath(libraryPath.getParent());
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

        logger.log(Level.FINEST, "OpenCV library \"{0}\" loaded from extracted copy at \"{1}\".", new Object[]{Core.NATIVE_LIBRARY_NAME, System.mapLibraryName(Core.NATIVE_LIBRARY_NAME)});
      }
    }

    /**
     * Cleans up patches done to the environment.
     */
    @Override
    protected void finalize() throws Throwable {
      super.finalize();

      if (null == libraryPath) {
        return;
      }

      removeLibraryPath(libraryPath.getParent());
    }

    private static class Holder {
      private static final SharedLoader INSTANCE = new SharedLoader();
    }

    public static SharedLoader getInstance() {
      return Holder.INSTANCE;
    }

    /**
     * Adds the provided {@link Path}, normalized, to the {@link ClassLoader#usr_paths} array, as well as to the {@code java.library.path} system property. Uses the reflection API to make the field accessible, and may be unsafe in environments with a security policy.
     *
     * @see Adding new paths for native libraries at runtime in Java
     */
    private static void addLibraryPath(final Path path) {
      final String normalizedPath = path.normalize().toString();

      try {
        final Field field = ClassLoader.class.getDeclaredField("usr_paths");
        field.setAccessible(true);

        final Set userPaths = new HashSet<>(Arrays.asList((String[]) field.get(null)));
        userPaths.add(normalizedPath);

        field.set(null, userPaths.toArray(new String[userPaths.size()]));

        System.setProperty("java.library.path", System.getProperty("java.library.path") + File.pathSeparator + normalizedPath);

        logger.log(Level.FINEST, "System library path now \"{0}\".", System.getProperty("java.library.path"));
      } catch (IllegalAccessException e) {
        throw new RuntimeException("Failed to get permissions to set library path");
      } catch (NoSuchFieldException e) {
        throw new RuntimeException("Failed to get field handle to set library path");
      }
    }

    /**
     * Removes the provided {@link Path}, normalized, from the {@link ClassLoader#usr_paths} array, as well as to the {@code java.library.path} system property. Uses the reflection API to make the field accessible, and may be unsafe in environments with a security policy.
     */
    private static void removeLibraryPath(final Path path) {
      final String normalizedPath = path.normalize().toString();

      try {
        final Field field = ClassLoader.class.getDeclaredField("usr_paths");
        field.setAccessible(true);

        final Set userPaths = new HashSet<>(Arrays.asList((String[]) field.get(null)));
        userPaths.remove(normalizedPath);

        field.set(null, userPaths.toArray(new String[userPaths.size()]));

        System.setProperty("java.library.path", System.getProperty("java.library.path").replace(File.pathSeparator + path.normalize().toString(), ""));
      } catch (IllegalAccessException e) {
        throw new RuntimeException("Failed to get permissions to set library path");
      } catch (NoSuchFieldException e) {
        throw new RuntimeException("Failed to get field handle to set library path");
      }
    }
  }

  /**
   * Exactly once per {@link ClassLoader}, extract the native binary from the classpath to a temporary location (which gets cleaned up on shutdown), and load that binary (via {@link System#load(String)}). Spurious calls are safe.
   */
  public static void loadLocally() {
    LocalLoader.getInstance();
  }

  private static class LocalLoader {
    private LocalLoader() {
      /* Retain this path for cleaning up later. */
      final Path libraryPath = extractNativeBinary();
      System.load(libraryPath.normalize().toString());

      logger.log(Level.FINEST, "OpenCV library \"{0}\" loaded from extracted copy at \"{1}\".", new Object[]{Core.NATIVE_LIBRARY_NAME, System.mapLibraryName(Core.NATIVE_LIBRARY_NAME)});
    }

    private static class Holder {
      private static final LocalLoader INSTANCE = new LocalLoader();
    }

    public static LocalLoader getInstance() {
      return Holder.INSTANCE;
    }
  }

  /**
   * Selects the appropriate packaged binary, extracts it to a temporary location (which gets deleted when the JVM shuts down), and returns a {@link Path} to that file.
   */
  @CallerSensitive
  private static Path extractNativeBinary() {
    final OS os = OS.getCurrent();
    final Arch arch = Arch.getCurrent();
    return extractNativeBinary(os, arch);
  }

  /**
   * Extracts the packaged binary for the specified platform to a temporary location (which gets deleted when the JVM shuts down), and returns a {@link Path} to that file.
   */
  private static Path extractNativeBinary(final OS os, final Arch arch) {
    final String location;

    switch (os) {
      case LINUX:
        switch (arch) {
          case X86_32:
            location = "/nu/pattern/opencv/linux/x86_32/libopencv_java2411.so";
            break;
          case X86_64:
            location = "/nu/pattern/opencv/linux/x86_64/libopencv_java2411.so";
            break;
          default:
            throw new UnsupportedPlatformException(os, arch);
        }
        break;
      case OSX:
        switch (arch) {
          case X86_64:
            location = "/nu/pattern/opencv/osx/x86_64/libopencv_java2411.dylib";
            break;
          default:
            throw new UnsupportedPlatformException(os, arch);
        }
        break;
      case WINDOWS:
          switch (arch) {
            case X86_32:
              location = "/nu/pattern/opencv/windows/x86_32/opencv_java2411.dll";
              break;
            case X86_64:
              location = "/nu/pattern/opencv/windows/x86_64/opencv_java2411.dll";
              break;
            default:
              throw new UnsupportedPlatformException(os, arch);
          }
          break;
      default:
        throw new UnsupportedPlatformException(os, arch);
    }

    logger.log(Level.FINEST, "Selected native binary \"{0}\".", location);

    final InputStream binary = OpenCV.class.getResourceAsStream(location);
    final Path destination = new TemporaryDirectory().markDeleteOnExit().getPath().resolve("./" + location).normalize();

    try {
      logger.log(Level.FINEST, "Copying native binary to \"{0}\".", destination);
      Files.createDirectories(destination.getParent());
      Files.copy(binary, destination);
    } catch (final IOException ioe) {
      throw new IllegalStateException(String.format("Error writing native library to \"%s\".", destination), ioe);
    }

    logger.log(Level.FINEST, "Extracted native binary to \"{0}\".", destination);

    return destination;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy