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

com.fizzed.mediaj.ImageProber Maven / Gradle / Ivy

/*
 * Copyright 2019 Fizzed, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.fizzed.mediaj;

import com.fizzed.crux.mediatype.KnownMediaType;
import com.fizzed.crux.util.Base16;
import com.fizzed.crux.util.Size2D;
import com.fizzed.mediaj.core.ByteArrayImageInputStream;
import com.fizzed.mediaj.core.StreamingSVGDocument;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;

public class ImageProber {
 
    static private final int MAGIC_MAX_LENGTH;
    static private final Map MAGIC_NUMBERS;
    static {
        MAGIC_NUMBERS = new LinkedHashMap<>();
        MAGIC_NUMBERS.put(Base16.decode("ffd8ff"), KnownMediaType.IMAGE_JPEG);
        MAGIC_NUMBERS.put(Base16.decode("89504e470d0a1a0a"), KnownMediaType.IMAGE_PNG);
        MAGIC_NUMBERS.put(Base16.decode("25504446"), KnownMediaType.APPLICATION_PDF);
        MAGIC_NUMBERS.put(Base16.decode("474946383961"), KnownMediaType.IMAGE_GIF);
        MAGIC_NUMBERS.put(Base16.decode("474946383761"), KnownMediaType.IMAGE_GIF);
        MAGIC_NUMBERS.put(Base16.decode("524946460000000057454250"), KnownMediaType.IMAGE_WEBP);

        // what's the max number of bytes we need to analyze?
        MAGIC_MAX_LENGTH = MAGIC_NUMBERS.keySet().stream()
            .mapToInt(v -> v.length)
            .max()
            .orElse(10);
    }
    
    /**
     * Probes the byte array to detect what kind of media type it contains.
     * Only a small subset of files are supported such as PNG, JPEG, PDF, etc.
     * @param data The byte array
     * @return The detected media type or null if none detected.
     * @throws IOException 
     */
    static public KnownMediaType probeMediaType(
            byte[] data) throws IOException {
        
        if (data == null || data.length == 0) {
            return null;
        }
        
        try (ByteArrayInputStream input = new ByteArrayInputStream(data)) {
            return probeMediaType(input);
        }
    }
    
    /**
     * Probes the file to detect what kind of media type it contains.
     * Only a small subset of files are supported such as PNG, JPEG, PDF, etc.
     * @param data The file to check
     * @return The detected media type or null if none detected.
     * @throws IOException 
     */
    static public KnownMediaType probeMediaType(
            Path file) throws IOException {
        
        if (file == null) {
            return null;
        }
        
        // must wrap it in a buffer to get mark support
        try (BufferedInputStream input = new BufferedInputStream(new FileInputStream(file.toFile()))) {
            return probeMediaType(input);
        }
    }
    
    static public KnownMediaType probeMediaType(
            InputStream input) throws IOException {
        
        // https://en.wikipedia.org/wiki/Magic_number_%28programming%29#Magic_numbers_in_files
        // jpeg: FF D8 FF
        // png: 89 50 4E 47 0D 0A 1A 0A
        // pdf: 25 50 44 46
        // gif: 47 49 46 38 39 61   OR    47 49 46 38 37 61
        
        byte[] bytes = new byte[MAGIC_MAX_LENGTH];
        input.mark(bytes.length);     // 10 bytes should do it
        try {
            for (int i = 0; i < bytes.length; i++) {
                int read = input.read(bytes, i, 1);
                if (read < 0) {
                    return null;        // unable to detect media type
                }
                MAGIC_LOOP:
                for (Map.Entry entry : MAGIC_NUMBERS.entrySet()) {
                    // only evaluate entries that we have a full length for
                    if (entry.getKey().length - 1 == i) {
                        // does it match?
                        for (int j = 0; j < entry.getKey().length; j++) {
                            // skip validating 0x00?
                            if (bytes[j] != (byte)0 && entry.getKey()[j] == (byte)0) {
                                // wildcard, allow it like it matched
                            }
                            else if (bytes[j] != entry.getKey()[j]) {
                                continue MAGIC_LOOP;  // move onto next
                            }
                        }
                        // it MUST have matched
                        return entry.getValue();
                    }
                }
            }
        } finally {
            input.reset();
        }
        
        return null;
    }
    
    static public Size2D probeSize(
            KnownMediaType mediaType,
            byte[] data) throws IOException {
        
        Objects.requireNonNull(mediaType, "mediaType was null");
        
            // special handling for svg
            if (mediaType == KnownMediaType.IMAGE_SVG_XML) {
                try (InputStream byteInput = new ByteArrayInputStream(data)) {
                    return StreamingSVGDocument.load(byteInput).getSize();
                }
            }

            // fallback to ImageIO
            try (ImageInputStream imageInput = new ByteArrayImageInputStream(data)) {
                return probeSize(mediaType, imageInput);
            }
    }
    
    static public Size2D probeSize(
            KnownMediaType mediaType,
            Path file) throws IOException {
        
        Objects.requireNonNull(file, "file was null");
        
        try (FileInputStream input = new FileInputStream(file.toFile())) {
            return probeSize(mediaType, input);
        }
    }
    
    static public Size2D probeSize(
            KnownMediaType mediaType,
            InputStream input) throws IOException {
        
        Objects.requireNonNull(mediaType, "mediaType was null");
        
        // special handling for svg
        if (mediaType == KnownMediaType.IMAGE_SVG_XML) {
            return StreamingSVGDocument.load(input).getSize();
        }
        
        try (ImageInputStream imageInput = new MemoryCacheImageInputStream(input)) {
            return probeSize(mediaType, imageInput);
        }
    }

    static private Size2D probeSize(
            KnownMediaType mediaType,
            ImageInputStream imageInput) throws IOException {
        
        Objects.requireNonNull(mediaType, "mediaType was null");
        Objects.requireNonNull(imageInput, "imageInput was null");
        
        Iterator iter = ImageIO.getImageReadersByMIMEType(mediaType.getLabel());
        if (iter.hasNext()) {
            ImageReader reader = iter.next();
            try {
                reader.setInput(imageInput);
                int width = reader.getWidth(reader.getMinIndex());
                int height = reader.getHeight(reader.getMinIndex());
                return new Size2D(width, height);
            } finally {
                reader.dispose();
            }
        } else {
            throw new IOException("Unable to probe dimension for media type " + mediaType);
        }
    }
    
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy