Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions mycore-iview2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mycore</groupId>
<artifactId>mycore-base</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* This file is part of *** M y C o R e ***
* See https://www.mycore.de/ for details.
*
* MyCoRe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MyCoRe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MyCoRe. If not, see <http://www.gnu.org/licenses/>.
*/

package org.mycore.iview2.backend;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import javax.imageio.ImageIO;

import org.mycore.common.MCRException;
import org.mycore.common.config.MCRConfiguration2;
import org.mycore.common.digest.MCRDigest;
import org.mycore.datamodel.niofs.MCRContentTypes;
import org.mycore.datamodel.niofs.MCRFileAttributes;
import org.mycore.datamodel.niofs.MCRPath;
import org.mycore.imagetiler.MCRImage;
import org.mycore.iview2.frontend.MCRIView2Commands;
import org.mycore.iview2.services.MCRIView2Tools;
import org.mycore.media.services.MCRVideoFrameGenerator;
import org.mycore.services.queuedjob.MCRJob;
import org.mycore.services.queuedjob.MCRJobAction;

/** Stores a full-resolution video frame as an iView image under the video's path. */
public class MCRVideoFrameJobAction extends MCRJobAction {

public static final String DERIVATE_PARAMETER = "derivate";

public static final String PATH_PARAMETER = "path";

public MCRVideoFrameJobAction(MCRJob job) {
super(job);
}

@Override
public boolean isActivated() {
return true;
}

@Override
public String name() {
return getClass().getName();
}

@Override
public void execute() {
MCRPath video = MCRPath.getPath(job.getParameter(DERIVATE_PARAMETER), job.getParameter(PATH_PARAMETER));
try {
if (!Files.isRegularFile(video)) {
return;
}
String contentType = MCRContentTypes.probeContentType(video);
if (contentType == null || !contentType.startsWith("video/")) {
return;
}
MCRVideoFrameGenerator generator = MCRConfiguration2.getInstanceOfOrThrow(
MCRVideoFrameGenerator.class, "MCR.Media.VideoFrameGenerator");
if (!generator.checkRequirements(video)) {
return;
}
// A replaced video changes its digest. The job of the new content then creates the frame.
MCRDigest digest = digest(video);
BufferedImage frame = generator.generateFrame(video);
Path image = Files.createTempFile("MyCoRe-Video-Frame-", ".png");
try {
if (!ImageIO.write(frame, "png", image.toFile())) {
throw new IOException("No PNG writer available");
}
// A queued video may have been deleted or replaced while FFmpeg was running.
if (!Files.isRegularFile(video) || !digest.equals(digest(video))) {
return;
}
MCRImage tiledImage = MCRImage.getInstance(image, video.getOwner(), video.getOwnerRelativePath());
tiledImage.setTileDir(MCRIView2Tools.getTileDir());
tiledImage.tile();
if (!Files.isRegularFile(video)) {
MCRIView2Commands.deleteImageTiles(video.getOwner(), video.getOwnerRelativePath());
}
} finally {
Files.deleteIfExists(image);
}
} catch (IOException e) {
throw new MCRException("Could not generate iView image for video " + video, e);
}
}

private static MCRDigest digest(MCRPath video) throws IOException {
return Files.readAttributes(video, MCRFileAttributes.class).digest();
}

@Override
public void rollback() {
// Files are cleaned up in execute(); a retry regenerates the frame.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* This file is part of *** M y C o R e ***
* See https://www.mycore.de/ for details.
*
* MyCoRe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MyCoRe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MyCoRe. If not, see <http://www.gnu.org/licenses/>.
*/

package org.mycore.iview2.events;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.mycore.common.MCRException;
import org.mycore.common.config.MCRConfiguration2;
import org.mycore.common.events.MCREvent;
import org.mycore.common.events.MCREventHandlerBase;
import org.mycore.datamodel.niofs.MCRContentTypes;
import org.mycore.datamodel.niofs.MCRPath;
import org.mycore.iview2.backend.MCRVideoFrameJobAction;
import org.mycore.iview2.frontend.MCRIView2Commands;
import org.mycore.iview2.services.MCRIView2Tools;
import org.mycore.media.services.MCRVideoFrameGenerator;
import org.mycore.services.queuedjob.MCRJob;
import org.mycore.services.queuedjob.MCRJobQueueManager;
import org.mycore.services.queuedjob.MCRJobStatus;

/** Creates iView images for every video file, independent of the derivate's main document. */
public class MCRVideoFrameEventHandler extends MCREventHandlerBase {

private static final Logger LOGGER = LogManager.getLogger();

private static final String VIDEO_FRAME_GENERATOR_PROPERTY = "MCR.Media.VideoFrameGenerator";

@Override
public void handlePathCreated(MCREvent evt, Path file, BasicFileAttributes attrs) {
if (!(file instanceof MCRPath path) || !attrs.isRegularFile()) {
return;
}
try {
String contentType = MCRContentTypes.probeContentType(path);
if (contentType != null && contentType.startsWith("video/") && checkRequirements(path)) {
MCRJob job = new MCRJob(MCRVideoFrameJobAction.class);
job.setParameter(MCRVideoFrameJobAction.DERIVATE_PARAMETER, path.getOwner());
job.setParameter(MCRVideoFrameJobAction.PATH_PARAMETER, path.getOwnerRelativePath());
MCRJobQueueManager manager = MCRJobQueueManager.getInstance();
if (manager.getJobDAO().getJobCount(MCRVideoFrameJobAction.class, job.getParameters(),
List.of(MCRJobStatus.NEW, MCRJobStatus.ERROR)) > 0) {
LOGGER.info("There is already a video frame job for {}. Skipping.", file);
} else if (!manager.getJobQueue(MCRVideoFrameJobAction.class).offer(job)) {
LOGGER.warn("Video frame job queue did not accept job for {}", file);
}
}
} catch (IOException | RuntimeException e) {
// The poster is optional and must not abort the file event.
LOGGER.warn("Could not enqueue video frame generation for {}", file, e);
}
}

private boolean checkRequirements(MCRPath path) {
return MCRConfiguration2.getInstanceOfOrThrow(MCRVideoFrameGenerator.class,
VIDEO_FRAME_GENERATOR_PROPERTY).checkRequirements(path);
}

@Override
public void handlePathUpdated(MCREvent evt, Path file, BasicFileAttributes attrs) {
handlePathCreated(evt, file, attrs);
}

@Override
public void handlePathRepaired(MCREvent evt, Path file, BasicFileAttributes attrs) {
// A repair only generates missing frames. Delete the tiles to force a new frame.
if (file instanceof MCRPath path && hasTiles(path)) {
return;
}
handlePathCreated(evt, file, attrs);
}

boolean hasTiles(MCRPath path) {
return MCRIView2Tools.hasTiles(path.getOwner(), path.getOwnerRelativePath());
}

@Override
public void handlePathDeleted(MCREvent evt, Path file, BasicFileAttributes attrs) {
if (file instanceof MCRPath path) {
// MIME detection need not work after deletion. The tiling handler also uses this cleanup.
try {
MCRIView2Commands.deleteImageTiles(path.getOwner(), path.getOwnerRelativePath());
} catch (IOException e) {
throw new MCRException("Could not delete video frame tiles for " + file, e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
import org.mycore.datamodel.niofs.MCRPath;
import org.mycore.imagetiler.MCRImage;
import org.mycore.imagetiler.MCRTiledPictureProps;
import org.mycore.iview2.backend.MCRTileFileProvider;
import org.mycore.iview2.backend.MCRTileInfo;

import jakarta.activation.FileTypeMap;
import jakarta.persistence.EntityManager;
Expand Down Expand Up @@ -152,6 +154,20 @@ public static boolean isFileSupported(String filename) {
filename.toLowerCase(Locale.ROOT)));
}

/**
* Checks whether an iView tile file exists for a file in a derivate.
*
* @param derivateId the derivate ID
* @param path the file path within the derivate
* @return true if a regular tile file exists
*/
public static boolean hasTiles(String derivateId, String path) {
MCRTileFileProvider provider = MCRConfiguration2
.getSingleInstanceOfOrThrow(MCRTileFileProvider.class, "MCR.IIIFImage.Iview.TileFileProvider");
return provider.getTileFile(new MCRTileInfo(derivateId, path, null))
.filter(Files::isRegularFile).isPresent();
}

/**
* Checks for a given derivate id whether all files in that derivate are tiled.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

package org.mycore.iview2.services;

import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
Expand Down Expand Up @@ -59,13 +62,26 @@ public class MCRIview2URIResolver implements URIResolver {
*/
@Override
public Source resolve(String href, String base) throws TransformerException {
String[] params = href.split(":");
String[] params = href.split(":", 3);

if (params.length != 3) {
throw new TransformerException("Invalid href: " + href);
}

switch (params[1]) {
case "hasTiles" -> {
int separator = params[2].indexOf('/');
if (separator <= 0 || separator == params[2].length() - 1) {
throw new TransformerException("Invalid href: " + href);
}
String derivateId = params[2].substring(0, separator);
try {
String path = URLDecoder.decode(params[2].substring(separator + 1), StandardCharsets.UTF_8);
return new JDOMSource(new Element(String.valueOf(MCRIView2Tools.hasTiles(derivateId, path))));
} catch (IllegalArgumentException e) {
throw new TransformerException("Invalid encoded path in href: " + href, e);
}
}
case "isCompletelyTiled" -> {
boolean completelyTiled = MCRIView2Tools.isCompletelyTiled(params[2]);
return new JDOMSource(new Element(String.valueOf(completelyTiled)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ MCR.IIIFImage.Iview.ThumbnailForPdfEventHandler.Derivate.Types=derivate_types:co
MCR.EventHandler.MCRDerivate.080.Class=org.mycore.iview2.events.MCRThumbnailForPdfEventHandler
MCR.Media.Thumbnail.Generators=%MCR.Media.Thumbnail.Generators%,org.mycore.iview2.services.MCRImageThumbnailGenerator

MCR.QueuedJob.OrdinaryJobs=%MCR.QueuedJob.OrdinaryJobs%,org.mycore.iview2.backend.MCRPDFThumbnailJobAction
MCR.QueuedJob.OrdinaryJobs=%MCR.QueuedJob.OrdinaryJobs%,org.mycore.iview2.backend.MCRPDFThumbnailJobAction,org.mycore.iview2.backend.MCRVideoFrameJobAction

# Maximum input file size in bytes for PDF thumbnail generation. If empty, no limit is applied.
MCR.IView2.PDF.Thumbnail.MaxInputFileSize=

# Generate an iView image from an embedded cover or representative full-resolution frame of every video file.
MCR.EventHandler.MCRPath.025.Class=org.mycore.iview2.events.MCRVideoFrameEventHandler
8 changes: 8 additions & 0 deletions mycore-iview2/src/main/resources/xslt/functions/iview2.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,12 @@
<xsl:value-of select="count($isCompletelyTiled/true)&gt;0" />
</xsl:function>

<!-- Checks availability only; IIIF enforces permissions when serving the image. -->
<xsl:function name="mcriview2:has-tiles" as="xs:boolean">
<xsl:param name="derivateID" as="xs:string"/>
<xsl:param name="path" as="xs:string"/>
<xsl:sequence select="exists(document(concat('iview2:hasTiles:', $derivateID, '/',
encode-for-uri($path)))/true)"/>
</xsl:function>

</xsl:stylesheet>
Loading