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

org.codelibs.fess.thumbnail.playwright.PlaywrightThumbnailGenerator Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2012-2024 CodeLibs Project and the Others.
 *
 * 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 org.codelibs.fess.thumbnail.playwright;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codelibs.core.exception.IORuntimeException;
import org.codelibs.core.lang.StringUtil;
import org.codelibs.core.misc.Tuple4;
import org.codelibs.fess.Constants;
import org.codelibs.fess.crawler.exception.CrawlerSystemException;
import org.codelibs.fess.mylasta.direction.FessConfig;
import org.codelibs.fess.thumbnail.impl.BaseThumbnailGenerator;
import org.codelibs.fess.util.ComponentUtil;

import com.microsoft.playwright.Browser;
import com.microsoft.playwright.Browser.NewContextOptions;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.BrowserType.LaunchOptions;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Page.ScreenshotOptions;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.Response;
import com.microsoft.playwright.options.LoadState;

public class PlaywrightThumbnailGenerator extends BaseThumbnailGenerator {

    private static final Logger logger = LogManager.getLogger(PlaywrightThumbnailGenerator.class);

    protected NewContextOptions newContextOptions = new NewContextOptions();

    protected LoadState renderedState = LoadState.NETWORKIDLE;

    protected Map options = new HashMap<>();

    protected LaunchOptions launchOptions;

    protected String browserName = "chromium";

    protected int closeTimeout = 15; // 15s

    protected int viewportWidth = 960;

    protected int viewportHeight = 960;

    protected boolean loadFullPage = false;

    protected Tuple4 worker;

    protected Page.NavigateOptions navigateOptions;

    protected double navigationTimeout = 30000;

    @PostConstruct
    public void init() {
        final String lastaEnv = System.getProperty("lasta.env");
        if (!Constants.EXECUTE_TYPE_THUMBNAIL.equals(lastaEnv)) {
            if (logger.isDebugEnabled()) {
                logger.debug("lasta.env is {}. PlaywrightThumbnailGenerator is disabled.", lastaEnv);
            }
            return;
        }

        createWorker();
    }

    protected void createWorker() {
        if (logger.isDebugEnabled()) {
            logger.debug("Initiaizing Playwright...");
        }

        updateProperties();

        Playwright playwright = null;
        Browser browser = null;
        BrowserContext browserContext = null;
        Page page = null;
        try {
            playwright = Playwright.create(new Playwright.CreateOptions().setEnv(options));
            browser = getBrowserType(playwright).launch(launchOptions);
            browserContext = browser.newContext(newContextOptions);
            page = browserContext.newPage();
            page.setViewportSize(viewportWidth, viewportHeight);
            worker = new Tuple4<>(playwright, browser, browserContext, page);
            navigateOptions = new Page.NavigateOptions().setTimeout(navigationTimeout);
            available = true;
        } catch (final Exception e) {
            available = false;
            if (logger.isDebugEnabled()) {
                logger.debug("Failed to create Playwright instance.", e);
            }
            close(playwright, browser, browserContext, page);
            throw new CrawlerSystemException("Failed to create PlaywrightThumbnailGenerator.", e);
        }
    }

    protected void updateProperties() {
        final FessConfig fessConfig = ComponentUtil.getFessConfig();
        final String viewportWidthStr = fessConfig.getSystemProperty("thumbnail.playwright.viewport.width");
        if (viewportWidthStr != null) {
            viewportWidth = Integer.valueOf(viewportWidthStr);
        }
        final String viewportHeightStr = fessConfig.getSystemProperty("thumbnail.playwright.viewport.height");
        if (viewportHeightStr != null) {
            viewportHeight = Integer.valueOf(viewportHeightStr);
        }
        final String navigationTimeoutStr = fessConfig.getSystemProperty("thumbnail.playwright.navigation.timeout");
        if (navigationTimeoutStr != null) {
            navigationTimeout = Double.valueOf(navigationTimeoutStr);
        }
    }

    protected BrowserType getBrowserType(final Playwright playwright) {
        if (logger.isDebugEnabled()) {
            logger.debug("Create {}...", browserName);
        }
        return switch (browserName) {
        case "firefox":
            yield playwright.firefox();
        case "webkit":
            yield playwright.webkit();
        case "chromium":
            yield playwright.chromium();
        default:
            throw new CrawlerSystemException("Unknown browser name: " + browserName);
        };
    }

    @Override
    public boolean generate(final String thumbnailId, final File outputFile) {
        if (worker == null) {
            logger.warn("[{}] Playwright is not availavle.", thumbnailId);
            return false;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Generate Thumbnail: {}", thumbnailId);
        }

        if (outputFile.exists()) {
            if (logger.isDebugEnabled()) {
                logger.debug("The thumbnail file exists: {}", outputFile.getAbsolutePath());
            }
            return true;
        }

        final File parentFile = outputFile.getParentFile();
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }
        if (!parentFile.isDirectory()) {
            logger.warn("Not found: {}", parentFile.getAbsolutePath());
            return false;
        }

        return process(thumbnailId, (configId, url) -> {
            boolean created = false;
            final FessConfig fessConfig = ComponentUtil.getFessConfig();
            final int targetWidth = fessConfig.getThumbnailHtmlImageThumbnailWidthAsInteger();
            final int maxHeight = fessConfig.getThumbnailHtmlImageThumbnailHeightAsInteger();
            try {
                createScreenshot(url, targetWidth, maxHeight, outputFile);
                created = true;
            } catch (final Throwable t) {
                logger.warn("Failed to create thumbnail: {} -> {} ({}:{})", thumbnailId, url, t.getClass().getCanonicalName(),
                        t.getMessage());
                if (logger.isDebugEnabled()) {
                    logger.debug("Details for failed thumbnail creation.", t);
                }
            } finally {
                if (!created) {
                    updateThumbnailField(thumbnailId, StringUtil.EMPTY);
                    if (outputFile.exists() && !outputFile.delete()) {
                        logger.warn("Failed to delete {}", outputFile.getAbsolutePath());
                    }
                }
            }
            return outputFile.exists();
        });
    }

    protected synchronized void createScreenshot(final String url, final int width, final int height, final File outputFile) {
        final Page page = worker.getValue4();
        File tempPngFile = null;
        try {
            final Response response = page.navigate(url, navigateOptions);
            page.waitForLoadState(renderedState);
            if (logger.isDebugEnabled()) {
                logger.debug("Loaded {} -> {}", url, response.url());
            }

            tempPngFile = ComponentUtil.getSystemHelper().createTempFile("fess-thumbnail-", ".png");
            page.screenshot(createScreenshotOptions(tempPngFile));

            if (logger.isDebugEnabled()) {
                logger.debug("Saved screenshot: {}", tempPngFile.getAbsolutePath());
            }

            // Load the original screenshot
            final BufferedImage img = ImageIO.read(tempPngFile);

            final int imageWidth = img.getWidth();
            final int imageHeight = img.getHeight();
            if (logger.isDebugEnabled()) {
                logger.debug("Screenshot is {}x{}", imageWidth, imageHeight);
            }

            // Calculate the target height to maintain the aspect ratio
            final int targetHeight = (int) ((double) imageHeight / imageWidth * width);

            // Create a new image with the target dimensions
            BufferedImage resizedImg = new BufferedImage(width, targetHeight, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g2d = null;
            try {
                g2d = resizedImg.createGraphics();
                g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                g2d.drawImage(img, 0, 0, width, targetHeight, null);
            } finally {
                if (g2d != null) {
                    g2d.dispose();
                }
            }

            if (logger.isDebugEnabled()) {
                logger.debug("Resized screenshot is {}x{}", resizedImg.getWidth(), resizedImg.getHeight());
            }

            // If the resized image is taller than the maximum height, clip it
            if (resizedImg.getHeight() > height) {
                resizedImg = resizedImg.getSubimage(0, 0, width, height);
            }

            // Save the resized/clipped image
            ImageIO.write(resizedImg, "png", outputFile);
        } catch (final IOException e) {
            throw new IORuntimeException(e);
        } finally {
            if (tempPngFile != null) {
                tempPngFile.delete();
            }
        }
    }

    protected ScreenshotOptions createScreenshotOptions(final File tempPngFile) {
        return new Page.ScreenshotOptions().setFullPage(loadFullPage).setPath(tempPngFile.toPath());
    }

    @Override
    public void destroy() {
        if (worker != null) {
            close(worker.getValue1(), worker.getValue2(), worker.getValue3(), worker.getValue4());
        }
    }

    protected void close(final Playwright playwright, final Browser browser, final BrowserContext context, final Page page) {
        closeInBackground(() -> {
            if (page != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Closing Page...");
                }
                page.close();
            }
        });
        closeInBackground(() -> {
            if (context != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Closing BrowserContext...");
                }
                context.close();
            }
        });
        closeInBackground(() -> {
            if (browser != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Closing Browser...");
                }
                browser.close();
            }
        });
        closeInBackground(() -> {
            if (playwright != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Closing Playwright...");
                }
                playwright.close();
            }
        });
    }

    protected void closeInBackground(final Runnable closer) {
        final CountDownLatch latch = new CountDownLatch(1);
        try {
            final Thread thread = new Thread(() -> {
                try {
                    closer.run();
                } catch (final Exception e) {
                    logger.warn("Failed to close the playwright instance.", e);
                }
                latch.countDown();
            }, "Playwright-Closer");
            thread.setDaemon(true);
            thread.start();
            if (!latch.await(closeTimeout, TimeUnit.SECONDS)) {
                logger.warn("The close process is timed out.");
            }
        } catch (final InterruptedException e) {
            logger.warn("Interrupted to wait a process.", e);
        } catch (final Exception e) {
            logger.warn("Failed to close the playwright instance.", e);
        }
    }

    public void setLaunchOptions(final LaunchOptions launchOptions) {
        this.launchOptions = launchOptions;
    }

    public void setBrowserName(final String browserName) {
        this.browserName = browserName;
    }

    public void setRenderedState(final LoadState loadState) {
        this.renderedState = loadState;
    }

    public void setCloseTimeout(final int closeTimeout) {
        this.closeTimeout = closeTimeout;
    }

    public void setNewContextOptions(final NewContextOptions newContextOptions) {
        this.newContextOptions = newContextOptions;
    }

    public void setViewportWidth(int viewportWidth) {
        this.viewportWidth = viewportWidth;
    }

    public void setViewportHeight(int viewportHeight) {
        this.viewportHeight = viewportHeight;
    }

    public void setLoadFullPage(boolean loadFullPage) {
        this.loadFullPage = loadFullPage;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy