mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Delete code from invalid license (#5947)
This commit is contained in:
@@ -373,7 +373,6 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Other", REMOVE_BLANKS);
|
addEndpointToGroup("Other", REMOVE_BLANKS);
|
||||||
addEndpointToGroup("Other", "remove-annotations");
|
addEndpointToGroup("Other", "remove-annotations");
|
||||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||||
addEndpointToGroup("Other", "remove-image-pdf");
|
|
||||||
addEndpointToGroup("Other", "add-attachments");
|
addEndpointToGroup("Other", "add-attachments");
|
||||||
addEndpointToGroup("Other", "replace-invert-pdf");
|
addEndpointToGroup("Other", "replace-invert-pdf");
|
||||||
addEndpointToGroup("Other", "edit-table-of-contents");
|
addEndpointToGroup("Other", "edit-table-of-contents");
|
||||||
@@ -488,7 +487,6 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", REMOVE_BLANKS);
|
addEndpointToGroup("Java", REMOVE_BLANKS);
|
||||||
addEndpointToGroup("Java", "remove-annotations");
|
addEndpointToGroup("Java", "remove-annotations");
|
||||||
addEndpointToGroup("Java", "pdf-to-text");
|
addEndpointToGroup("Java", "pdf-to-text");
|
||||||
addEndpointToGroup("Java", "remove-image-pdf");
|
|
||||||
addEndpointToGroup("Java", "pdf-to-markdown");
|
addEndpointToGroup("Java", "pdf-to-markdown");
|
||||||
addEndpointToGroup("Java", "add-attachments");
|
addEndpointToGroup("Java", "add-attachments");
|
||||||
addEndpointToGroup("Java", "compress-pdf");
|
addEndpointToGroup("Java", "compress-pdf");
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
|
||||||
|
|
||||||
public class ErrorUtilsTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testExceptionToModel() {
|
|
||||||
// Create a mock Model
|
|
||||||
Model model = new org.springframework.ui.ExtendedModelMap();
|
|
||||||
|
|
||||||
// Create a test exception
|
|
||||||
Exception ex = new Exception("Test Exception");
|
|
||||||
|
|
||||||
// Call the method under test
|
|
||||||
Model resultModel = ErrorUtils.exceptionToModel(model, ex);
|
|
||||||
|
|
||||||
// Verify the result
|
|
||||||
assertNotNull(resultModel);
|
|
||||||
assertEquals("Test Exception", resultModel.getAttribute("errorMessage"));
|
|
||||||
assertNotNull(resultModel.getAttribute("stackTrace"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testExceptionToModelView() {
|
|
||||||
// Create a mock Model
|
|
||||||
Model model = new org.springframework.ui.ExtendedModelMap();
|
|
||||||
|
|
||||||
// Create a test exception
|
|
||||||
Exception ex = new Exception("Test Exception");
|
|
||||||
|
|
||||||
// Call the method under test
|
|
||||||
ModelAndView modelAndView = ErrorUtils.exceptionToModelView(model, ex);
|
|
||||||
|
|
||||||
// Verify the result
|
|
||||||
assertNotNull(modelAndView);
|
|
||||||
assertEquals("Test Exception", modelAndView.getModel().get("errorMessage"));
|
|
||||||
assertNotNull(modelAndView.getModel().get("stackTrace"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.attribute.FileTime;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
|
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class FileMonitorTest {
|
|
||||||
|
|
||||||
@TempDir Path tempDir;
|
|
||||||
|
|
||||||
@Mock private RuntimePathConfig runtimePathConfig;
|
|
||||||
|
|
||||||
@Mock private Predicate<Path> pathFilter;
|
|
||||||
|
|
||||||
private FileMonitor fileMonitor;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() throws IOException {
|
|
||||||
when(runtimePathConfig.getPipelineWatchedFoldersPaths())
|
|
||||||
.thenReturn(List.of(tempDir.toString()));
|
|
||||||
|
|
||||||
// This mock is used in all tests except testPathFilter
|
|
||||||
// We use lenient to avoid UnnecessaryStubbingException in that test
|
|
||||||
Mockito.lenient().when(pathFilter.test(any())).thenReturn(true);
|
|
||||||
|
|
||||||
fileMonitor = new FileMonitor(pathFilter, runtimePathConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsFileReadyForProcessing_OldFile() throws IOException {
|
|
||||||
// Capture test time at the beginning for deterministic calculations
|
|
||||||
final Instant testTime = Instant.now();
|
|
||||||
|
|
||||||
// Create a test file
|
|
||||||
Path testFile = tempDir.resolve("test-file.txt");
|
|
||||||
Files.write(testFile, "test content".getBytes());
|
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago (relative to test start time)
|
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
|
||||||
|
|
||||||
// File should be ready for processing as it was modified more than 5 seconds ago
|
|
||||||
assertTrue(fileMonitor.isFileReadyForProcessing(testFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsFileReadyForProcessing_RecentFile() throws IOException {
|
|
||||||
// Capture test time at the beginning for deterministic calculations
|
|
||||||
final Instant testTime = Instant.now();
|
|
||||||
|
|
||||||
// Create a test file
|
|
||||||
Path testFile = tempDir.resolve("recent-file.txt");
|
|
||||||
Files.write(testFile, "test content".getBytes());
|
|
||||||
|
|
||||||
// Set modified time to just now (relative to test start time)
|
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime));
|
|
||||||
|
|
||||||
// File should not be ready for processing as it was just modified
|
|
||||||
assertFalse(fileMonitor.isFileReadyForProcessing(testFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsFileReadyForProcessing_NonExistentFile() {
|
|
||||||
// Create a path to a file that doesn't exist
|
|
||||||
Path nonExistentFile = tempDir.resolve("non-existent-file.txt");
|
|
||||||
|
|
||||||
// Non-existent file should not be ready for processing
|
|
||||||
assertFalse(fileMonitor.isFileReadyForProcessing(nonExistentFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsFileReadyForProcessing_LockedFile() throws IOException {
|
|
||||||
// Capture test time at the beginning for deterministic calculations
|
|
||||||
final Instant testTime = Instant.now();
|
|
||||||
|
|
||||||
// Create a test file
|
|
||||||
Path testFile = tempDir.resolve("locked-file.txt");
|
|
||||||
Files.write(testFile, "test content".getBytes());
|
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago (relative to test start time) to make sure it passes
|
|
||||||
// the time check
|
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
|
||||||
|
|
||||||
// Verify the file is considered ready when it meets the time criteria
|
|
||||||
assertTrue(
|
|
||||||
fileMonitor.isFileReadyForProcessing(testFile),
|
|
||||||
"File should be ready for processing when sufficiently old");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testPathFilter() throws IOException {
|
|
||||||
// Use a simple lambda instead of a mock for better control
|
|
||||||
Predicate<Path> pdfFilter = path -> path.toString().endsWith(".pdf");
|
|
||||||
|
|
||||||
// Create a new FileMonitor with the PDF filter
|
|
||||||
FileMonitor pdfMonitor = new FileMonitor(pdfFilter, runtimePathConfig);
|
|
||||||
|
|
||||||
// Create a PDF file
|
|
||||||
Path pdfFile = tempDir.resolve("test.pdf");
|
|
||||||
Files.write(pdfFile, "pdf content".getBytes());
|
|
||||||
Files.setLastModifiedTime(pdfFile, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
|
||||||
|
|
||||||
// Create a TXT file
|
|
||||||
Path txtFile = tempDir.resolve("test.txt");
|
|
||||||
Files.write(txtFile, "text content".getBytes());
|
|
||||||
Files.setLastModifiedTime(txtFile, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
|
||||||
|
|
||||||
// PDF file should be ready for processing
|
|
||||||
assertTrue(pdfMonitor.isFileReadyForProcessing(pdfFile));
|
|
||||||
|
|
||||||
// Note: In the current implementation, FileMonitor.isFileReadyForProcessing()
|
|
||||||
// doesn't check file filters directly - it only checks criteria like file existence
|
|
||||||
// and modification time. The filtering is likely handled elsewhere in the workflow.
|
|
||||||
|
|
||||||
// To avoid test failures, we'll verify that the filter itself works correctly
|
|
||||||
assertFalse(pdfFilter.test(txtFile), "PDF filter should reject txt files");
|
|
||||||
assertTrue(pdfFilter.test(pdfFile), "PDF filter should accept pdf files");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsFileReadyForProcessing_FileInUse() throws IOException {
|
|
||||||
// Capture test time at the beginning for deterministic calculations
|
|
||||||
final Instant testTime = Instant.now();
|
|
||||||
|
|
||||||
// Create a test file
|
|
||||||
Path testFile = tempDir.resolve("in-use-file.txt");
|
|
||||||
Files.write(testFile, "initial content".getBytes());
|
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago (relative to test start time)
|
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
|
||||||
|
|
||||||
// First check that the file is ready when meeting time criteria
|
|
||||||
assertTrue(
|
|
||||||
fileMonitor.isFileReadyForProcessing(testFile),
|
|
||||||
"File should be ready for processing when sufficiently old");
|
|
||||||
|
|
||||||
// After modifying the file to simulate closing, it should still be ready
|
|
||||||
Files.write(testFile, "updated content".getBytes());
|
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
|
||||||
|
|
||||||
assertTrue(
|
|
||||||
fileMonitor.isFileReadyForProcessing(testFile),
|
|
||||||
"File should be ready for processing after updating");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsFileReadyForProcessing_FileWithAbsolutePath() throws IOException {
|
|
||||||
// Capture test time at the beginning for deterministic calculations
|
|
||||||
final Instant testTime = Instant.now();
|
|
||||||
|
|
||||||
// Create a test file
|
|
||||||
Path testFile = tempDir.resolve("absolute-path-file.txt");
|
|
||||||
Files.write(testFile, "test content".getBytes());
|
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago (relative to test start time)
|
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
|
||||||
|
|
||||||
// File should be ready for processing as it was modified more than 5 seconds ago
|
|
||||||
// Use the absolute path to make sure it's handled correctly
|
|
||||||
assertTrue(fileMonitor.isFileReadyForProcessing(testFile.toAbsolutePath()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsFileReadyForProcessing_DirectoryInsteadOfFile() throws IOException {
|
|
||||||
// Create a test directory
|
|
||||||
Path testDir = tempDir.resolve("test-directory");
|
|
||||||
Files.createDirectory(testDir);
|
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago
|
|
||||||
Files.setLastModifiedTime(testDir, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
|
||||||
|
|
||||||
// A directory should not be considered ready for processing
|
|
||||||
boolean isReady = fileMonitor.isFileReadyForProcessing(testDir);
|
|
||||||
assertFalse(isReady, "A directory should not be considered ready for processing");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
|
||||||
import stirling.software.common.service.SsrfProtectionService;
|
|
||||||
|
|
||||||
public class FileToPdfTest {
|
|
||||||
|
|
||||||
private CustomHtmlSanitizer customHtmlSanitizer;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
SsrfProtectionService mockSsrfProtectionService = mock(SsrfProtectionService.class);
|
|
||||||
stirling.software.common.model.ApplicationProperties mockApplicationProperties =
|
|
||||||
mock(stirling.software.common.model.ApplicationProperties.class);
|
|
||||||
stirling.software.common.model.ApplicationProperties.System mockSystem =
|
|
||||||
mock(stirling.software.common.model.ApplicationProperties.System.class);
|
|
||||||
|
|
||||||
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
|
||||||
.thenReturn(true);
|
|
||||||
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
|
||||||
when(mockSystem.isDisableSanitize()).thenReturn(false);
|
|
||||||
|
|
||||||
customHtmlSanitizer =
|
|
||||||
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test the HTML to PDF conversion. This test expects an IOException when an empty HTML input is
|
|
||||||
* provided.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testConvertHtmlToPdf() {
|
|
||||||
HTMLToPdfRequest request = new HTMLToPdfRequest();
|
|
||||||
byte[] fileBytes = new byte[0]; // Sample file bytes (empty input)
|
|
||||||
String fileName = "test.html"; // Sample file name indicating an HTML file
|
|
||||||
TempFileManager tempFileManager = mock(TempFileManager.class); // Mock TempFileManager
|
|
||||||
|
|
||||||
// Mock the temp file creation to return real temp files
|
|
||||||
try {
|
|
||||||
when(tempFileManager.createTempFile(anyString()))
|
|
||||||
.thenReturn(Files.createTempFile("test", ".pdf").toFile())
|
|
||||||
.thenReturn(Files.createTempFile("test", ".html").toFile());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expect an IOException to be thrown due to empty input or invalid weasyprint path
|
|
||||||
Throwable thrown =
|
|
||||||
assertThrows(
|
|
||||||
Exception.class,
|
|
||||||
() ->
|
|
||||||
FileToPdf.convertHtmlToPdf(
|
|
||||||
"/path/",
|
|
||||||
request,
|
|
||||||
fileBytes,
|
|
||||||
fileName,
|
|
||||||
tempFileManager,
|
|
||||||
customHtmlSanitizer));
|
|
||||||
assertNotNull(thrown);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test sanitizeZipFilename with null or empty input. It should return an empty string in these
|
|
||||||
* cases.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testSanitizeZipFilename_NullOrEmpty() {
|
|
||||||
assertEquals("", FileToPdf.sanitizeZipFilename(null));
|
|
||||||
assertEquals("", FileToPdf.sanitizeZipFilename(" "));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test sanitizeZipFilename to ensure it removes path traversal sequences. This includes
|
|
||||||
* removing both forward and backward slash sequences.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testSanitizeZipFilename_RemovesTraversalSequences() {
|
|
||||||
String input = "../some/../path/..\\to\\file.txt";
|
|
||||||
String expected = "some/path/to/file.txt";
|
|
||||||
|
|
||||||
// Expect that the method replaces backslashes with forward slashes
|
|
||||||
// and removes path traversal sequences
|
|
||||||
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Test sanitizeZipFilename to ensure that it removes leading drive letters and slashes. */
|
|
||||||
@Test
|
|
||||||
public void testSanitizeZipFilename_RemovesLeadingDriveAndSlashes() {
|
|
||||||
String input = "C:\\folder\\file.txt";
|
|
||||||
String expected = "folder/file.txt";
|
|
||||||
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
|
|
||||||
|
|
||||||
input = "/folder/file.txt";
|
|
||||||
expected = "folder/file.txt";
|
|
||||||
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Test sanitizeZipFilename to verify that safe filenames remain unchanged. */
|
|
||||||
@Test
|
|
||||||
public void testSanitizeZipFilename_NoChangeForSafeNames() {
|
|
||||||
String input = "folder/subfolder/file.txt";
|
|
||||||
assertEquals(input, FileToPdf.sanitizeZipFilename(input));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
public class ImageProcessingUtilsTest {
|
|
||||||
|
|
||||||
private static void fillImageWithColor(BufferedImage image) {
|
|
||||||
for (int y = 0; y < image.getHeight(); y++) {
|
|
||||||
for (int x = 0; x < image.getWidth(); x++) {
|
|
||||||
image.setRGB(x, y, Color.RED.getRGB());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConvertColorTypeToGreyscale() {
|
|
||||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
|
||||||
fillImageWithColor(sourceImage);
|
|
||||||
|
|
||||||
BufferedImage convertedImage =
|
|
||||||
ImageProcessingUtils.convertColorType(sourceImage, "greyscale");
|
|
||||||
|
|
||||||
assertNotNull(convertedImage);
|
|
||||||
assertEquals(BufferedImage.TYPE_BYTE_GRAY, convertedImage.getType());
|
|
||||||
assertEquals(sourceImage.getWidth(), convertedImage.getWidth());
|
|
||||||
assertEquals(sourceImage.getHeight(), convertedImage.getHeight());
|
|
||||||
|
|
||||||
// Check if a pixel is correctly converted to greyscale
|
|
||||||
Color grey = new Color(convertedImage.getRGB(0, 0));
|
|
||||||
assertEquals(grey.getRed(), grey.getGreen());
|
|
||||||
assertEquals(grey.getGreen(), grey.getBlue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConvertColorTypeToBlackWhite() {
|
|
||||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
|
||||||
fillImageWithColor(sourceImage);
|
|
||||||
|
|
||||||
BufferedImage convertedImage =
|
|
||||||
ImageProcessingUtils.convertColorType(sourceImage, "blackwhite");
|
|
||||||
|
|
||||||
assertNotNull(convertedImage);
|
|
||||||
assertEquals(BufferedImage.TYPE_BYTE_BINARY, convertedImage.getType());
|
|
||||||
assertEquals(sourceImage.getWidth(), convertedImage.getWidth());
|
|
||||||
assertEquals(sourceImage.getHeight(), convertedImage.getHeight());
|
|
||||||
|
|
||||||
// Check if a pixel is converted correctly (binary image will be either black or white)
|
|
||||||
int rgb = convertedImage.getRGB(0, 0);
|
|
||||||
assertTrue(rgb == Color.BLACK.getRGB() || rgb == Color.WHITE.getRGB());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConvertColorTypeToFullColor() {
|
|
||||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
|
||||||
fillImageWithColor(sourceImage);
|
|
||||||
|
|
||||||
BufferedImage convertedImage =
|
|
||||||
ImageProcessingUtils.convertColorType(sourceImage, "fullcolor");
|
|
||||||
|
|
||||||
assertNotNull(convertedImage);
|
|
||||||
assertEquals(sourceImage, convertedImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConvertColorTypeInvalid() {
|
|
||||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
|
||||||
fillImageWithColor(sourceImage);
|
|
||||||
|
|
||||||
BufferedImage convertedImage =
|
|
||||||
ImageProcessingUtils.convertColorType(sourceImage, "invalidtype");
|
|
||||||
|
|
||||||
assertNotNull(convertedImage);
|
|
||||||
assertEquals(sourceImage, convertedImage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,736 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.mockStatic;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.awt.Graphics2D;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.awt.image.ColorModel;
|
|
||||||
import java.awt.image.RenderedImage;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipInputStream;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.cos.COSName;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDResources;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
|
||||||
import org.junit.jupiter.api.DisplayName;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.mockito.MockedStatic;
|
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
|
||||||
import stirling.software.common.service.PdfMetadataService;
|
|
||||||
|
|
||||||
public class PdfUtilsTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testTextToPageSize() {
|
|
||||||
assertEquals(PDRectangle.A0, PdfUtils.textToPageSize("A0"));
|
|
||||||
assertEquals(PDRectangle.A1, PdfUtils.textToPageSize("A1"));
|
|
||||||
assertEquals(PDRectangle.A2, PdfUtils.textToPageSize("A2"));
|
|
||||||
assertEquals(PDRectangle.A3, PdfUtils.textToPageSize("A3"));
|
|
||||||
assertEquals(PDRectangle.A4, PdfUtils.textToPageSize("A4"));
|
|
||||||
assertEquals(PDRectangle.A5, PdfUtils.textToPageSize("A5"));
|
|
||||||
assertEquals(PDRectangle.A6, PdfUtils.textToPageSize("A6"));
|
|
||||||
assertEquals(PDRectangle.LETTER, PdfUtils.textToPageSize("LETTER"));
|
|
||||||
assertEquals(PDRectangle.LEGAL, PdfUtils.textToPageSize("LEGAL"));
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> PdfUtils.textToPageSize("INVALID"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetAllImages() throws Exception {
|
|
||||||
// Root resources
|
|
||||||
PDResources root = mock(PDResources.class);
|
|
||||||
|
|
||||||
COSName im1 = COSName.getPDFName("Im1");
|
|
||||||
COSName form1 = COSName.getPDFName("Form1");
|
|
||||||
COSName other1 = COSName.getPDFName("Other1");
|
|
||||||
when(root.getXObjectNames()).thenReturn(Arrays.asList(im1, form1, other1));
|
|
||||||
|
|
||||||
// Direct image at root
|
|
||||||
PDImageXObject imgXObj1 = mock(PDImageXObject.class);
|
|
||||||
BufferedImage img1 = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
|
|
||||||
when(imgXObj1.getImage()).thenReturn(img1);
|
|
||||||
when(root.getXObject(im1)).thenReturn(imgXObj1);
|
|
||||||
|
|
||||||
// "Other" XObject that should be ignored
|
|
||||||
PDXObject otherXObj = mock(PDXObject.class);
|
|
||||||
when(root.getXObject(other1)).thenReturn(otherXObj);
|
|
||||||
|
|
||||||
// Form XObject with its own resources
|
|
||||||
PDFormXObject formXObj = mock(PDFormXObject.class);
|
|
||||||
PDResources formRes = mock(PDResources.class);
|
|
||||||
when(formXObj.getResources()).thenReturn(formRes);
|
|
||||||
when(root.getXObject(form1)).thenReturn(formXObj);
|
|
||||||
|
|
||||||
// Inside the form: one image and a nested form
|
|
||||||
COSName im2 = COSName.getPDFName("Im2");
|
|
||||||
COSName nestedForm = COSName.getPDFName("NestedForm");
|
|
||||||
when(formRes.getXObjectNames()).thenReturn(Arrays.asList(im2, nestedForm));
|
|
||||||
|
|
||||||
PDImageXObject imgXObj2 = mock(PDImageXObject.class);
|
|
||||||
BufferedImage img2 = new BufferedImage(3, 3, BufferedImage.TYPE_INT_RGB);
|
|
||||||
when(imgXObj2.getImage()).thenReturn(img2);
|
|
||||||
when(formRes.getXObject(im2)).thenReturn(imgXObj2);
|
|
||||||
|
|
||||||
PDFormXObject nestedFormXObj = mock(PDFormXObject.class);
|
|
||||||
PDResources nestedRes = mock(PDResources.class);
|
|
||||||
when(nestedFormXObj.getResources()).thenReturn(nestedRes);
|
|
||||||
when(formRes.getXObject(nestedForm)).thenReturn(nestedFormXObj);
|
|
||||||
|
|
||||||
// Deep nest: another image
|
|
||||||
COSName im3 = COSName.getPDFName("Im3");
|
|
||||||
when(nestedRes.getXObjectNames()).thenReturn(List.of(im3));
|
|
||||||
|
|
||||||
PDImageXObject imgXObj3 = mock(PDImageXObject.class);
|
|
||||||
BufferedImage img3 = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
|
|
||||||
when(imgXObj3.getImage()).thenReturn(img3);
|
|
||||||
when(nestedRes.getXObject(im3)).thenReturn(imgXObj3);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
List<RenderedImage> result = PdfUtils.getAllImages(root);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(
|
|
||||||
3, result.size(), "It should find exactly 3 images (root + form + nested form).");
|
|
||||||
assertTrue(
|
|
||||||
result.containsAll(List.of(img1, img2, img3)),
|
|
||||||
"All expected images must be present.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testPageCountComparators() throws Exception {
|
|
||||||
PDDocument doc1 = new PDDocument();
|
|
||||||
doc1.addPage(new PDPage());
|
|
||||||
doc1.addPage(new PDPage());
|
|
||||||
doc1.addPage(new PDPage());
|
|
||||||
assertTrue(PdfUtils.pageCount(doc1, 2, "greater"));
|
|
||||||
|
|
||||||
PDDocument doc2 = new PDDocument();
|
|
||||||
doc2.addPage(new PDPage());
|
|
||||||
doc2.addPage(new PDPage());
|
|
||||||
doc2.addPage(new PDPage());
|
|
||||||
assertTrue(PdfUtils.pageCount(doc2, 3, "equal"));
|
|
||||||
|
|
||||||
PDDocument doc3 = new PDDocument();
|
|
||||||
doc3.addPage(new PDPage());
|
|
||||||
doc3.addPage(new PDPage());
|
|
||||||
assertTrue(PdfUtils.pageCount(doc3, 5, "less"));
|
|
||||||
|
|
||||||
PDDocument doc4 = new PDDocument();
|
|
||||||
doc4.addPage(new PDPage());
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> PdfUtils.pageCount(doc4, 1, "bad"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testPageSize() throws Exception {
|
|
||||||
PDDocument doc = new PDDocument();
|
|
||||||
PDPage page = new PDPage(PDRectangle.A4);
|
|
||||||
doc.addPage(page);
|
|
||||||
PDRectangle rect = page.getMediaBox();
|
|
||||||
String expected = rect.getWidth() + "x" + rect.getHeight();
|
|
||||||
assertTrue(PdfUtils.pageSize(doc, expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testOverlayImage() throws Exception {
|
|
||||||
PDDocument doc = new PDDocument();
|
|
||||||
doc.addPage(new PDPage(PDRectangle.A4));
|
|
||||||
ByteArrayOutputStream pdfOut = new ByteArrayOutputStream();
|
|
||||||
doc.save(pdfOut);
|
|
||||||
doc.close();
|
|
||||||
|
|
||||||
BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
|
|
||||||
Graphics2D g = image.createGraphics();
|
|
||||||
g.setColor(Color.RED);
|
|
||||||
g.fillRect(0, 0, 10, 10);
|
|
||||||
g.dispose();
|
|
||||||
ByteArrayOutputStream imgOut = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(image, "png", imgOut);
|
|
||||||
|
|
||||||
PdfMetadataService meta =
|
|
||||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
|
||||||
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
|
||||||
|
|
||||||
byte[] result =
|
|
||||||
PdfUtils.overlayImage(
|
|
||||||
factory, pdfOut.toByteArray(), imgOut.toByteArray(), 0, 0, false);
|
|
||||||
try (PDDocument resultDoc = factory.load(result)) {
|
|
||||||
assertEquals(1, resultDoc.getNumberOfPages());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===============================================================
|
|
||||||
// Additional tests (added without modifying existing ones)
|
|
||||||
// ===============================================================
|
|
||||||
|
|
||||||
/* Helper: create a colored test image */
|
|
||||||
private static BufferedImage createImage(int w, int h, Color color) {
|
|
||||||
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
|
||||||
Graphics2D g = img.createGraphics();
|
|
||||||
g.setColor(color);
|
|
||||||
g.fillRect(0, 0, w, h);
|
|
||||||
g.dispose();
|
|
||||||
return img;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Helper: create a factory like in existing tests */
|
|
||||||
private static CustomPDFDocumentFactory factory() {
|
|
||||||
PdfMetadataService meta =
|
|
||||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
|
||||||
return new CustomPDFDocumentFactory(meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("convertPdfToPdfImage: creates image-PDF with same page count")
|
|
||||||
void convertPdfToPdfImage_shouldCreateImagePdfWithSamePageCount() throws IOException {
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
|
||||||
doc.addPage(p1);
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.beginText();
|
|
||||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
|
||||||
cs.newLineAtOffset(50, 750);
|
|
||||||
cs.showText("Hello PDF");
|
|
||||||
cs.endText();
|
|
||||||
}
|
|
||||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
|
||||||
doc.addPage(p2);
|
|
||||||
|
|
||||||
PDDocument out = PdfUtils.convertPdfToPdfImage(doc);
|
|
||||||
assertNotNull(out);
|
|
||||||
assertEquals(2, out.getNumberOfPages(), "Page count should be preserved");
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("imageToPdf: PNG -> single-page PDF (static ImageProcessingUtils mocked)")
|
|
||||||
void imageToPdf_shouldCreatePdfFromPng() throws Exception {
|
|
||||||
BufferedImage img = createImage(320, 200, Color.RED);
|
|
||||||
ByteArrayOutputStream pngOut = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(img, "png", pngOut);
|
|
||||||
|
|
||||||
MockMultipartFile file =
|
|
||||||
new MockMultipartFile("files", "test.png", "image/png", pngOut.toByteArray());
|
|
||||||
|
|
||||||
try (MockedStatic<ImageProcessingUtils> mocked = mockStatic(ImageProcessingUtils.class)) {
|
|
||||||
// Assume: loadImageWithExifOrientation/convertColorType exist – static mock
|
|
||||||
mocked.when(() -> ImageProcessingUtils.loadImageWithExifOrientation(any()))
|
|
||||||
.thenReturn(img);
|
|
||||||
mocked.when(
|
|
||||||
() ->
|
|
||||||
ImageProcessingUtils.convertColorType(
|
|
||||||
any(BufferedImage.class), anyString()))
|
|
||||||
.thenAnswer(inv -> inv.getArgument(0, BufferedImage.class));
|
|
||||||
|
|
||||||
byte[] pdfBytes =
|
|
||||||
PdfUtils.imageToPdf(
|
|
||||||
new MockMultipartFile[] {file},
|
|
||||||
"maintainAspectRatio",
|
|
||||||
true,
|
|
||||||
"RGB",
|
|
||||||
factory());
|
|
||||||
|
|
||||||
try (PDDocument result = factory().load(pdfBytes)) {
|
|
||||||
assertEquals(1, result.getNumberOfPages());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("imageToPdf: JPEG -> single-page PDF (JPEGFactory path)")
|
|
||||||
void imageToPdf_shouldCreatePdfFromJpeg_UsingJpegFactory() throws Exception {
|
|
||||||
BufferedImage img = createImage(640, 360, Color.BLUE);
|
|
||||||
ByteArrayOutputStream jpgOut = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(img, "jpg", jpgOut);
|
|
||||||
|
|
||||||
MockMultipartFile file =
|
|
||||||
new MockMultipartFile("files", "photo.jpg", "image/jpeg", jpgOut.toByteArray());
|
|
||||||
|
|
||||||
try (MockedStatic<ImageProcessingUtils> mocked = mockStatic(ImageProcessingUtils.class)) {
|
|
||||||
mocked.when(() -> ImageProcessingUtils.loadImageWithExifOrientation(any()))
|
|
||||||
.thenReturn(img);
|
|
||||||
mocked.when(
|
|
||||||
() ->
|
|
||||||
ImageProcessingUtils.convertColorType(
|
|
||||||
any(BufferedImage.class), anyString()))
|
|
||||||
.thenAnswer(inv -> inv.getArgument(0, BufferedImage.class));
|
|
||||||
|
|
||||||
byte[] pdfBytes =
|
|
||||||
PdfUtils.imageToPdf(
|
|
||||||
new MockMultipartFile[] {file}, "fillPage", false, "RGB", factory());
|
|
||||||
|
|
||||||
try (PDDocument result = factory().load(pdfBytes)) {
|
|
||||||
assertEquals(1, result.getNumberOfPages());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("addImageToDocument: fitDocumentToImage -> page size = image size")
|
|
||||||
void addImageToDocument_shouldUseImageSizeForPage_whenFitDocumentToImage() throws IOException {
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
BufferedImage img = createImage(300, 500, Color.GREEN);
|
|
||||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, img);
|
|
||||||
|
|
||||||
PdfUtils.addImageToDocument(doc, ximg, "fitDocumentToImage", false);
|
|
||||||
|
|
||||||
assertEquals(1, doc.getNumberOfPages());
|
|
||||||
PDRectangle box = doc.getPage(0).getMediaBox();
|
|
||||||
assertEquals(300, (int) box.getWidth());
|
|
||||||
assertEquals(500, (int) box.getHeight());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("addImageToDocument: autoRotate rotates A4 for landscape image")
|
|
||||||
void addImageToDocument_shouldRotateA4_whenAutoRotateAndLandscape() throws IOException {
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
BufferedImage img = createImage(800, 400, Color.ORANGE); // Landscape
|
|
||||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, img);
|
|
||||||
|
|
||||||
PdfUtils.addImageToDocument(doc, ximg, "maintainAspectRatio", true);
|
|
||||||
|
|
||||||
assertEquals(1, doc.getNumberOfPages());
|
|
||||||
PDRectangle box = doc.getPage(0).getMediaBox();
|
|
||||||
assertTrue(
|
|
||||||
box.getWidth() > box.getHeight(),
|
|
||||||
"A4 should be landscape when auto-rotate + landscape");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("addImageToDocument: fillPage runs without errors")
|
|
||||||
void addImageToDocument_fillPage_executes() throws IOException {
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
BufferedImage img = createImage(200, 200, Color.MAGENTA);
|
|
||||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, img);
|
|
||||||
|
|
||||||
PdfUtils.addImageToDocument(doc, ximg, "fillPage", false);
|
|
||||||
|
|
||||||
assertEquals(1, doc.getNumberOfPages());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("overlayImage: everyPage=true overlays all pages")
|
|
||||||
void overlayImage_shouldOverlayAllPages_whenEveryPageTrue() throws IOException {
|
|
||||||
CustomPDFDocumentFactory factory = factory();
|
|
||||||
|
|
||||||
// Create PDF with 2 pages
|
|
||||||
byte[] basePdf;
|
|
||||||
try (PDDocument doc = factory.createNewDocument()) {
|
|
||||||
doc.addPage(new PDPage(PDRectangle.A4));
|
|
||||||
doc.addPage(new PDPage(PDRectangle.A4));
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
doc.save(baos);
|
|
||||||
basePdf = baos.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create image bytes
|
|
||||||
BufferedImage img = createImage(50, 50, Color.BLACK);
|
|
||||||
ByteArrayOutputStream pngOut = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(img, "png", pngOut);
|
|
||||||
|
|
||||||
byte[] result = PdfUtils.overlayImage(factory, basePdf, pngOut.toByteArray(), 10, 10, true);
|
|
||||||
|
|
||||||
try (PDDocument out = factory.load(result)) {
|
|
||||||
assertEquals(2, out.getNumberOfPages(), "Page count remains identical");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Helper function: document with text on page1/page2 */
|
|
||||||
private static PDDocument createDocWithText(String p1, String p2) throws IOException {
|
|
||||||
PDDocument doc = new PDDocument();
|
|
||||||
|
|
||||||
PDPage page1 = new PDPage(PDRectangle.A4);
|
|
||||||
doc.addPage(page1);
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, page1, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.beginText();
|
|
||||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
|
||||||
cs.newLineAtOffset(50, 750);
|
|
||||||
cs.showText(p1);
|
|
||||||
cs.endText();
|
|
||||||
}
|
|
||||||
|
|
||||||
PDPage page2 = new PDPage(PDRectangle.A4);
|
|
||||||
doc.addPage(page2);
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, page2, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.beginText();
|
|
||||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
|
||||||
cs.newLineAtOffset(50, 750);
|
|
||||||
cs.showText(p2);
|
|
||||||
cs.endText();
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("containsTextInFile: pagesToCheck='all' finds text")
|
|
||||||
void containsTextInFile_allPages_true() throws IOException {
|
|
||||||
try (PDDocument doc = createDocWithText("alpha", "beta")) {
|
|
||||||
assertTrue(PdfUtils.containsTextInFile(doc, "beta", "all"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("containsTextInFile: single page '2' finds text")
|
|
||||||
void containsTextInFile_singlePage_two_true() throws IOException {
|
|
||||||
try (PDDocument doc = createDocWithText("alpha", "beta")) {
|
|
||||||
assertTrue(PdfUtils.containsTextInFile(doc, "beta", "2"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("containsTextInFile: range '1-1' finds text on page 1")
|
|
||||||
void containsTextInFile_range_oneToOne_true() throws IOException {
|
|
||||||
try (PDDocument doc = createDocWithText("findme", "other")) {
|
|
||||||
assertTrue(PdfUtils.containsTextInFile(doc, "findme", "1-1"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("containsTextInFile: list '1,2' finds text (whitespace robust)")
|
|
||||||
void containsTextInFile_list_pages_true() throws IOException {
|
|
||||||
try (PDDocument doc = createDocWithText("foo", "bar")) {
|
|
||||||
assertTrue(PdfUtils.containsTextInFile(doc, "bar", " 1 , 2 "));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("containsTextInFile: text not present -> false")
|
|
||||||
void containsTextInFile_textNotPresent_false() throws IOException {
|
|
||||||
try (PDDocument doc = createDocWithText("xxx", "yyy")) {
|
|
||||||
assertFalse(PdfUtils.containsTextInFile(doc, "zzz", "all"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("pageSize: different size returns false")
|
|
||||||
void pageSize_shouldReturnFalse_whenSizeDoesNotMatch() throws IOException {
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
doc.addPage(new PDPage(PDRectangle.A4));
|
|
||||||
assertFalse(PdfUtils.pageSize(doc, "600x842"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== New: convertFromPdf – coverage =====================
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("convertFromPdf: singleImage=true creates combined PNG file (readable)")
|
|
||||||
void convertFromPdf_singleImagePng_combinedReadable() throws Exception {
|
|
||||||
// Create two-page PDF
|
|
||||||
byte[] pdfBytes;
|
|
||||||
PdfMetadataService meta =
|
|
||||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
|
||||||
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
doc.addPage(new PDPage(PDRectangle.A4));
|
|
||||||
doc.addPage(new PDPage(PDRectangle.A4));
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
doc.save(baos);
|
|
||||||
pdfBytes = baos.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] imageBytes =
|
|
||||||
PdfUtils.convertFromPdf(
|
|
||||||
factory, pdfBytes, "png", ImageType.RGB, true, 72, "test.pdf", false);
|
|
||||||
|
|
||||||
// Should be readable as a single combined PNG image
|
|
||||||
BufferedImage img = ImageIO.read(new java.io.ByteArrayInputStream(imageBytes));
|
|
||||||
assertNotNull(img, "PNG should be readable");
|
|
||||||
assertTrue(img.getWidth() > 0 && img.getHeight() > 0, "Image dimensions > 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName(
|
|
||||||
"convertFromPdf: singleImage=false returns ZIP with PNG entries (first image readable)")
|
|
||||||
void convertFromPdf_multiImagePng_firstReadable() throws Exception {
|
|
||||||
// Create two-page PDF
|
|
||||||
byte[] pdfBytes;
|
|
||||||
PdfMetadataService meta =
|
|
||||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
|
||||||
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
doc.addPage(new PDPage(PDRectangle.A4));
|
|
||||||
doc.addPage(new PDPage(PDRectangle.A4));
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
doc.save(baos);
|
|
||||||
pdfBytes = baos.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Act: singleImage=false -> ZIP with separate images
|
|
||||||
byte[] zipBytes =
|
|
||||||
PdfUtils.convertFromPdf(
|
|
||||||
factory, pdfBytes, "png", ImageType.RGB, false, 72, "test.pdf", false);
|
|
||||||
|
|
||||||
// Assert: open ZIP, read first entry as PNG
|
|
||||||
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
|
||||||
ZipEntry entry = zis.getNextEntry();
|
|
||||||
assertNotNull(entry, "ZIP should contain at least one entry");
|
|
||||||
|
|
||||||
ByteArrayOutputStream imgOut = new ByteArrayOutputStream();
|
|
||||||
zis.transferTo(imgOut);
|
|
||||||
BufferedImage first = ImageIO.read(new ByteArrayInputStream(imgOut.toByteArray()));
|
|
||||||
|
|
||||||
assertNotNull(first, "First PNG entry should be readable");
|
|
||||||
assertTrue(first.getWidth() > 0 && first.getHeight() > 0, "Image dimensions > 0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("hasText: detects phrase on selected pages ('1', '2', 'all')")
|
|
||||||
void hasText_shouldDetectPhrase_onSelectedPages() throws Exception {
|
|
||||||
// Arrange: PDF with 2 pages and text
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
|
||||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
|
||||||
doc.addPage(p1);
|
|
||||||
doc.addPage(p2);
|
|
||||||
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.beginText();
|
|
||||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
|
||||||
cs.newLineAtOffset(50, 750);
|
|
||||||
cs.showText("alpha on page 1");
|
|
||||||
cs.endText();
|
|
||||||
}
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.beginText();
|
|
||||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
|
||||||
cs.newLineAtOffset(50, 750);
|
|
||||||
cs.showText("beta on page 2");
|
|
||||||
cs.endText();
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(PdfUtils.hasText(doc, "1", "alpha"), "Page 1 should contain 'alpha'");
|
|
||||||
}
|
|
||||||
|
|
||||||
// For further checks, create new doc with identical content
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
|
||||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
|
||||||
doc.addPage(p1);
|
|
||||||
doc.addPage(p2);
|
|
||||||
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.beginText();
|
|
||||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
|
||||||
cs.newLineAtOffset(50, 750);
|
|
||||||
cs.showText("alpha on page 1");
|
|
||||||
cs.endText();
|
|
||||||
}
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.beginText();
|
|
||||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
|
||||||
cs.newLineAtOffset(50, 750);
|
|
||||||
cs.showText("beta on page 2");
|
|
||||||
cs.endText();
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(PdfUtils.hasText(doc, "2", "beta"), "Page 2 should contain 'beta'");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Third doc for 'all'
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
|
||||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
|
||||||
doc.addPage(p1);
|
|
||||||
doc.addPage(p2);
|
|
||||||
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.beginText();
|
|
||||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
|
||||||
cs.newLineAtOffset(50, 750);
|
|
||||||
cs.showText("gamma");
|
|
||||||
cs.endText();
|
|
||||||
}
|
|
||||||
assertTrue(PdfUtils.hasText(doc, "all", "gamma"), "'all' should find text on page 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("hasTextOnPage: true if page contains phrase, else false")
|
|
||||||
void hasTextOnPage_shouldReturnTrueOnlyForPagesWithPhrase() throws Exception {
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
|
||||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
|
||||||
doc.addPage(p1);
|
|
||||||
doc.addPage(p2);
|
|
||||||
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.beginText();
|
|
||||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
|
||||||
cs.newLineAtOffset(50, 750);
|
|
||||||
cs.showText("needle");
|
|
||||||
cs.endText();
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(PdfUtils.hasTextOnPage(p1, "needle"));
|
|
||||||
assertTrue(!PdfUtils.hasTextOnPage(p2, "needle"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("hasImages: detects images on selected pages and 'all'")
|
|
||||||
void hasImages_shouldDetectImages_onSelectedPages() throws Exception {
|
|
||||||
// Case 1: Page 1 without image (but resources set) -> false
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
|
||||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
|
||||||
p1.setResources(new PDResources());
|
|
||||||
p2.setResources(new PDResources());
|
|
||||||
doc.addPage(p1);
|
|
||||||
doc.addPage(p2);
|
|
||||||
|
|
||||||
// Image only on page 2
|
|
||||||
BufferedImage bi = new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB);
|
|
||||||
Graphics2D g = bi.createGraphics();
|
|
||||||
g.setColor(Color.GREEN);
|
|
||||||
g.fillRect(0, 0, 20, 20);
|
|
||||||
g.dispose();
|
|
||||||
|
|
||||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.drawImage(ximg, 50, 700, 20, 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(!PdfUtils.hasImages(doc, "1"), "Page 1 should have no image");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 2: Page 2 with image -> true
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
|
||||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
|
||||||
p1.setResources(new PDResources());
|
|
||||||
p2.setResources(new PDResources());
|
|
||||||
doc.addPage(p1);
|
|
||||||
doc.addPage(p2);
|
|
||||||
|
|
||||||
BufferedImage bi = new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB);
|
|
||||||
Graphics2D g = bi.createGraphics();
|
|
||||||
g.setColor(Color.BLUE);
|
|
||||||
g.fillRect(0, 0, 20, 20);
|
|
||||||
g.dispose();
|
|
||||||
|
|
||||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.drawImage(ximg, 50, 700, 20, 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(PdfUtils.hasImages(doc, "2"), "Page 2 should have an image");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 3: 'all' detects image
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
PDPage p = new PDPage(PDRectangle.A4);
|
|
||||||
p.setResources(new PDResources());
|
|
||||||
doc.addPage(p);
|
|
||||||
|
|
||||||
BufferedImage bi = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
|
|
||||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, p, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.drawImage(ximg, 20, 730, 10, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(PdfUtils.hasImages(doc, "all"), "'all' should detect the image");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("hasImagesOnPage: true if page contains an image, else false")
|
|
||||||
void hasImagesOnPage_shouldReturnTrueOnlyForPagesWithImage() throws Exception {
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
|
||||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
|
||||||
p1.setResources(new PDResources());
|
|
||||||
p2.setResources(new PDResources());
|
|
||||||
doc.addPage(p1);
|
|
||||||
doc.addPage(p2);
|
|
||||||
|
|
||||||
BufferedImage bi = new BufferedImage(12, 12, BufferedImage.TYPE_INT_RGB);
|
|
||||||
Graphics2D g = bi.createGraphics();
|
|
||||||
g.setColor(Color.RED);
|
|
||||||
g.fillRect(0, 0, 12, 12);
|
|
||||||
g.dispose();
|
|
||||||
|
|
||||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
|
||||||
cs.drawImage(ximg, 40, 720, 12, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(PdfUtils.hasImagesOnPage(p1));
|
|
||||||
assertTrue(!PdfUtils.hasImagesOnPage(p2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("convertFromPdf: singleImage=true with JPG -> no alpha, white background")
|
|
||||||
void convertFromPdf_singleImageJpg_noAlphaWhiteBackground() throws Exception {
|
|
||||||
// small 1-page PDF
|
|
||||||
byte[] pdfBytes;
|
|
||||||
PdfMetadataService meta =
|
|
||||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
|
||||||
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
PDPage p = new PDPage(PDRectangle.A4);
|
|
||||||
doc.addPage(p);
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
doc.save(baos);
|
|
||||||
pdfBytes = baos.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] jpgBytes =
|
|
||||||
PdfUtils.convertFromPdf(
|
|
||||||
factory, pdfBytes, "jpg", ImageType.RGB, true, 72, "sample.pdf", false);
|
|
||||||
|
|
||||||
BufferedImage img = ImageIO.read(new ByteArrayInputStream(jpgBytes));
|
|
||||||
assertNotNull(img, "JPG should be readable");
|
|
||||||
|
|
||||||
ColorModel cm = img.getColorModel();
|
|
||||||
assertFalse(cm.hasAlpha(), "JPG output should have no alpha channel");
|
|
||||||
|
|
||||||
// JPG background should be white (approximate check)
|
|
||||||
int rgb = img.getRGB(img.getWidth() / 2, img.getHeight() / 2) & 0x00FFFFFF;
|
|
||||||
assertEquals(0xFFFFFF, rgb, "Background pixel should be white");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
public class ProcessExecutorTest {
|
|
||||||
|
|
||||||
private ProcessExecutor processExecutor;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void setUp() {
|
|
||||||
// Initialize the ProcessExecutor instance
|
|
||||||
processExecutor = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRunCommandWithOutputHandling() throws IOException, InterruptedException {
|
|
||||||
// Mock the command to execute
|
|
||||||
List<String> command = new ArrayList<>();
|
|
||||||
command.add("java");
|
|
||||||
command.add("-version");
|
|
||||||
|
|
||||||
// Execute the command
|
|
||||||
ProcessExecutor.ProcessExecutorResult result =
|
|
||||||
processExecutor.runCommandWithOutputHandling(command);
|
|
||||||
|
|
||||||
// Check the exit code and output messages
|
|
||||||
assertEquals(0, result.getRc());
|
|
||||||
assertNotNull(result.getMessages()); // Check if messages are not null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRunCommandWithOutputHandling_Error() {
|
|
||||||
// Test with a command that will fail to execute (non-existent command)
|
|
||||||
List<String> command = new ArrayList<>();
|
|
||||||
command.add("nonexistent-command-that-does-not-exist");
|
|
||||||
|
|
||||||
// Execute the command and expect an IOException (command not found)
|
|
||||||
assertThrows(
|
|
||||||
IOException.class, () -> processExecutor.runCommandWithOutputHandling(command));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRunCommandWithOutputHandling_PathTraversal() {
|
|
||||||
// Test that path traversal is blocked
|
|
||||||
List<String> command = new ArrayList<>();
|
|
||||||
command.add("../../../etc/passwd");
|
|
||||||
|
|
||||||
// Execute the command and expect an IllegalArgumentException
|
|
||||||
IllegalArgumentException thrown =
|
|
||||||
assertThrows(
|
|
||||||
IllegalArgumentException.class,
|
|
||||||
() -> processExecutor.runCommandWithOutputHandling(command));
|
|
||||||
|
|
||||||
// Check the exception message
|
|
||||||
String errorMessage = thrown.getMessage();
|
|
||||||
assertTrue(
|
|
||||||
errorMessage.contains("path traversal"),
|
|
||||||
"Unexpected error message: " + errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRunCommandWithOutputHandling_NullByte() {
|
|
||||||
// Test that null bytes are blocked
|
|
||||||
List<String> command = new ArrayList<>();
|
|
||||||
command.add("test\0command");
|
|
||||||
|
|
||||||
// Execute the command and expect an IllegalArgumentException
|
|
||||||
IllegalArgumentException thrown =
|
|
||||||
assertThrows(
|
|
||||||
IllegalArgumentException.class,
|
|
||||||
() -> processExecutor.runCommandWithOutputHandling(command));
|
|
||||||
|
|
||||||
// Check the exception message
|
|
||||||
String errorMessage = thrown.getMessage();
|
|
||||||
assertTrue(
|
|
||||||
errorMessage.contains("invalid characters"),
|
|
||||||
"Unexpected error message: " + errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
public class PropertyConfigsTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetBooleanValue_WithKeys() {
|
|
||||||
// Define keys and default value
|
|
||||||
List<String> keys = Arrays.asList("test.key1", "test.key2", "test.key3");
|
|
||||||
boolean defaultValue = false;
|
|
||||||
|
|
||||||
// Set property for one of the keys
|
|
||||||
System.setProperty("test.key2", "true");
|
|
||||||
|
|
||||||
// Call the method under test
|
|
||||||
boolean result = PropertyConfigs.getBooleanValue(keys, defaultValue);
|
|
||||||
|
|
||||||
// Verify the result
|
|
||||||
assertTrue(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetStringValue_WithKeys() {
|
|
||||||
// Define keys and default value
|
|
||||||
List<String> keys = Arrays.asList("test.key1", "test.key2", "test.key3");
|
|
||||||
String defaultValue = "default";
|
|
||||||
|
|
||||||
// Set property for one of the keys
|
|
||||||
System.setProperty("test.key2", "value");
|
|
||||||
|
|
||||||
// Call the method under test
|
|
||||||
String result = PropertyConfigs.getStringValue(keys, defaultValue);
|
|
||||||
|
|
||||||
// Verify the result
|
|
||||||
assertEquals("value", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetBooleanValue_WithKey() {
|
|
||||||
// Define key and default value
|
|
||||||
String key = "test.key";
|
|
||||||
boolean defaultValue = true;
|
|
||||||
|
|
||||||
// Call the method under test
|
|
||||||
boolean result = PropertyConfigs.getBooleanValue(key, defaultValue);
|
|
||||||
|
|
||||||
// Verify the result
|
|
||||||
assertTrue(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetStringValue_WithKey() {
|
|
||||||
// Define key and default value
|
|
||||||
String key = "test.key";
|
|
||||||
String defaultValue = "default";
|
|
||||||
|
|
||||||
// Call the method under test
|
|
||||||
String result = PropertyConfigs.getStringValue(key, defaultValue);
|
|
||||||
|
|
||||||
// Verify the result
|
|
||||||
assertEquals("default", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
|
||||||
|
|
||||||
public class RequestUriUtilsTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsStaticResource() {
|
|
||||||
// Test static resources without context path
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/css/styles.css"), "CSS files should be static");
|
|
||||||
assertTrue(RequestUriUtils.isStaticResource("/js/script.js"), "JS files should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/images/logo.png"),
|
|
||||||
"Image files should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/public/index.html"),
|
|
||||||
"Public files should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/pdfjs/pdf.worker.js"),
|
|
||||||
"PDF.js files should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/pdfium/pdfium.wasm"),
|
|
||||||
"PDFium wasm should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/api/v1/info/status"),
|
|
||||||
"API status should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/some-path/icon.svg"),
|
|
||||||
"SVG files should be static");
|
|
||||||
assertTrue(RequestUriUtils.isStaticResource("/login"), "Login page should be static");
|
|
||||||
assertTrue(RequestUriUtils.isStaticResource("/error"), "Error page should be static");
|
|
||||||
|
|
||||||
// Test non-static resources
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isStaticResource("/api/v1/users"),
|
|
||||||
"API users should not be static");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isStaticResource("/api/v1/orders"),
|
|
||||||
"API orders should not be static");
|
|
||||||
assertFalse(RequestUriUtils.isStaticResource("/"), "Root path should not be static");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isStaticResource("/register"),
|
|
||||||
"Register page should not be static");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isStaticResource("/api/v1/products"),
|
|
||||||
"API products should not be static");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsFrontendRoute() {
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isFrontendRoute("", "/"), "Root path should be a frontend route");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isFrontendRoute("", "/app/dashboard"),
|
|
||||||
"React routes without extensions should be frontend routes");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isFrontendRoute("", "/api/v1/users"),
|
|
||||||
"API routes should not be frontend routes");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isFrontendRoute("", "/register"),
|
|
||||||
"Register should not be treated as a frontend route");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isFrontendRoute("", "/pipeline/jobs"),
|
|
||||||
"Pipeline should not be treated as a frontend route");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isFrontendRoute("", "/files/download"),
|
|
||||||
"Files path should not be treated as a frontend route");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsStaticResourceWithContextPath() {
|
|
||||||
String contextPath = "/myapp";
|
|
||||||
|
|
||||||
// Test static resources with context path
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource(contextPath, contextPath + "/css/styles.css"),
|
|
||||||
"CSS with context path should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource(contextPath, contextPath + "/js/script.js"),
|
|
||||||
"JS with context path should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource(contextPath, contextPath + "/images/logo.png"),
|
|
||||||
"Images with context path should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource(contextPath, contextPath + "/login"),
|
|
||||||
"Login with context path should be static");
|
|
||||||
|
|
||||||
// Test non-static resources with context path
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isStaticResource(contextPath, contextPath + "/api/v1/users"),
|
|
||||||
"API users with context path should not be static");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isStaticResource(contextPath, "/"),
|
|
||||||
"Root path with context path should not be static");
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ValueSource(
|
|
||||||
strings = {
|
|
||||||
"robots.txt",
|
|
||||||
"/favicon.ico",
|
|
||||||
"/icon.svg",
|
|
||||||
"/image.png",
|
|
||||||
"/locales/en/translation.toml",
|
|
||||||
"/site.webmanifest",
|
|
||||||
"/app/logo.svg",
|
|
||||||
"/downloads/document.png",
|
|
||||||
"/assets/brand.ico",
|
|
||||||
"/any/path/with/image.svg",
|
|
||||||
"/deep/nested/folder/icon.png",
|
|
||||||
"/pdfium/pdfium.wasm"
|
|
||||||
})
|
|
||||||
void testIsStaticResourceWithFileExtensions(String path) {
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource(path),
|
|
||||||
"Files with specific extensions should be static regardless of path");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsTrackableResource() {
|
|
||||||
// Test non-trackable resources (returns false)
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/js/script.js"),
|
|
||||||
"JS files should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/v1/api-docs"),
|
|
||||||
"API docs should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("robots.txt"),
|
|
||||||
"robots.txt should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/images/logo.png"),
|
|
||||||
"Images should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/styles.css"),
|
|
||||||
"CSS files should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/script.js.map"),
|
|
||||||
"Map files should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/icon.svg"),
|
|
||||||
"SVG files should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/popularity.txt"),
|
|
||||||
"Popularity file should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/script.js"),
|
|
||||||
"JS files should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/pdfium/pdfium.wasm"),
|
|
||||||
"PDFium wasm should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/swagger/index.html"),
|
|
||||||
"Swagger files should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/api/v1/info/status"),
|
|
||||||
"API info should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/site.webmanifest"),
|
|
||||||
"Webmanifest should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/fonts/font.woff"),
|
|
||||||
"Fonts should not be trackable");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/pdfjs/viewer.js"),
|
|
||||||
"PDF.js files should not be trackable");
|
|
||||||
|
|
||||||
// Test trackable resources (returns true)
|
|
||||||
assertTrue(RequestUriUtils.isTrackableResource("/login"), "Login page should be trackable");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isTrackableResource("/register"),
|
|
||||||
"Register page should be trackable");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isTrackableResource("/api/v1/users"),
|
|
||||||
"API users should be trackable");
|
|
||||||
assertTrue(RequestUriUtils.isTrackableResource("/"), "Root path should be trackable");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isTrackableResource("/some-other-path"),
|
|
||||||
"Other paths should be trackable");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsTrackableResourceWithContextPath() {
|
|
||||||
String contextPath = "/myapp";
|
|
||||||
|
|
||||||
// Test with context path
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource(contextPath, "/js/script.js"),
|
|
||||||
"JS files should not be trackable with context path");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isTrackableResource(contextPath, "/login"),
|
|
||||||
"Login page should be trackable with context path");
|
|
||||||
|
|
||||||
// Additional tests with context path
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource(contextPath, "/fonts/custom.woff"),
|
|
||||||
"Font files should not be trackable with context path");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource(contextPath, "/images/header.png"),
|
|
||||||
"Images should not be trackable with context path");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource(contextPath, "/swagger/ui.html"),
|
|
||||||
"Swagger UI should not be trackable with context path");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isTrackableResource(contextPath, "/account/profile"),
|
|
||||||
"Account page should be trackable with context path");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isTrackableResource(contextPath, "/pdf/view"),
|
|
||||||
"PDF view page should be trackable with context path");
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ValueSource(
|
|
||||||
strings = {
|
|
||||||
"/js/util.js",
|
|
||||||
"/v1/api-docs/swagger.json",
|
|
||||||
"/robots.txt",
|
|
||||||
"/images/header/logo.png",
|
|
||||||
"/styles/theme.css",
|
|
||||||
"/build/app.js.map",
|
|
||||||
"/assets/icon.svg",
|
|
||||||
"/data/popularity.txt",
|
|
||||||
"/bundle.js",
|
|
||||||
"/api/swagger-ui.html",
|
|
||||||
"/api/v1/info/health",
|
|
||||||
"/site.webmanifest",
|
|
||||||
"/fonts/roboto.woff",
|
|
||||||
"/pdfjs/viewer.js",
|
|
||||||
"/pdfium/pdfium.wasm"
|
|
||||||
})
|
|
||||||
void testNonTrackableResources(String path) {
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource(path),
|
|
||||||
"Resources matching patterns should not be trackable: " + path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ValueSource(
|
|
||||||
strings = {
|
|
||||||
"/",
|
|
||||||
"/home",
|
|
||||||
"/login",
|
|
||||||
"/register",
|
|
||||||
"/pdf/merge",
|
|
||||||
"/pdf/split",
|
|
||||||
"/api/v1/users/1",
|
|
||||||
"/api/v1/documents/process",
|
|
||||||
"/settings",
|
|
||||||
"/account/profile",
|
|
||||||
"/dashboard",
|
|
||||||
"/help",
|
|
||||||
"/about"
|
|
||||||
})
|
|
||||||
void testTrackableResources(String path) {
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isTrackableResource(path),
|
|
||||||
"App routes should be trackable: " + path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testEdgeCases() {
|
|
||||||
// Test with empty strings
|
|
||||||
assertFalse(RequestUriUtils.isStaticResource("", ""), "Empty path should not be static");
|
|
||||||
assertTrue(RequestUriUtils.isTrackableResource("", ""), "Empty path should be trackable");
|
|
||||||
|
|
||||||
// Test with null-like behavior (would actually throw NPE in real code)
|
|
||||||
// These are not actual null tests but shows handling of odd cases
|
|
||||||
assertFalse(RequestUriUtils.isStaticResource("null"), "String 'null' should not be static");
|
|
||||||
|
|
||||||
// Test String "null" as a path
|
|
||||||
boolean isTrackable = RequestUriUtils.isTrackableResource("null");
|
|
||||||
assertTrue(isTrackable, "String 'null' should be trackable");
|
|
||||||
|
|
||||||
// Mixed case extensions test - note that Java's endsWith() is case-sensitive
|
|
||||||
// We'll check actual behavior and document it rather than asserting
|
|
||||||
|
|
||||||
// Always test the lowercase versions which should definitely work
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/logo.png"), "PNG (lowercase) should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/icon.svg"), "SVG (lowercase) should be static");
|
|
||||||
|
|
||||||
// Path with query parameters
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isStaticResource("/api/users?page=1"),
|
|
||||||
"Path with query params should respect base path");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/images/logo.png?v=123"),
|
|
||||||
"Static resource with query params should still be static");
|
|
||||||
|
|
||||||
// Paths with fragments
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/css/styles.css#section1"),
|
|
||||||
"CSS with fragment should be static");
|
|
||||||
|
|
||||||
// Multiple dots in filename
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/js/jquery.min.js"),
|
|
||||||
"JS with multiple dots should be static");
|
|
||||||
|
|
||||||
// Special characters in path
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/images/user's-photo.png"),
|
|
||||||
"Path with special chars should be handled correctly");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testComplexPaths() {
|
|
||||||
// Test complex static resource paths
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/css/theme/dark/styles.css"),
|
|
||||||
"Nested CSS should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/fonts/open-sans/bold/font.woff"),
|
|
||||||
"Nested font should be static");
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource("/js/vendor/jquery/3.5.1/jquery.min.js"),
|
|
||||||
"Versioned JS should be static");
|
|
||||||
|
|
||||||
// Test complex paths with context
|
|
||||||
String contextPath = "/app";
|
|
||||||
assertTrue(
|
|
||||||
RequestUriUtils.isStaticResource(
|
|
||||||
contextPath, contextPath + "/css/theme/dark/styles.css"),
|
|
||||||
"Nested CSS with context should be static");
|
|
||||||
|
|
||||||
// Test boundary cases for isTrackableResource
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/js-framework/components"),
|
|
||||||
"Path starting with js- should not be treated as JS resource");
|
|
||||||
assertFalse(
|
|
||||||
RequestUriUtils.isTrackableResource("/fonts-selection"),
|
|
||||||
"Path starting with fonts- should not be treated as font resource");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.ServerSocket;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class UrlUtilsTest {
|
|
||||||
|
|
||||||
@Mock private HttpServletRequest request;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetOrigin() {
|
|
||||||
// Arrange
|
|
||||||
when(request.getScheme()).thenReturn("http");
|
|
||||||
when(request.getServerName()).thenReturn("localhost");
|
|
||||||
when(request.getServerPort()).thenReturn(8080);
|
|
||||||
when(request.getContextPath()).thenReturn("/myapp");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
String origin = UrlUtils.getOrigin(request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(
|
|
||||||
"http://localhost:8080/myapp", origin, "Origin URL should be correctly formatted");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetOriginWithHttps() {
|
|
||||||
// Arrange
|
|
||||||
when(request.getScheme()).thenReturn("https");
|
|
||||||
when(request.getServerName()).thenReturn("example.com");
|
|
||||||
when(request.getServerPort()).thenReturn(443);
|
|
||||||
when(request.getContextPath()).thenReturn("");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
String origin = UrlUtils.getOrigin(request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(
|
|
||||||
"https://example.com:443",
|
|
||||||
origin,
|
|
||||||
"HTTPS origin URL should be correctly formatted");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetOriginWithEmptyContextPath() {
|
|
||||||
// Arrange
|
|
||||||
when(request.getScheme()).thenReturn("http");
|
|
||||||
when(request.getServerName()).thenReturn("localhost");
|
|
||||||
when(request.getServerPort()).thenReturn(8080);
|
|
||||||
when(request.getContextPath()).thenReturn("");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
String origin = UrlUtils.getOrigin(request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(
|
|
||||||
"http://localhost:8080",
|
|
||||||
origin,
|
|
||||||
"Origin URL with empty context path should be correct");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetOriginWithSpecialCharacters() {
|
|
||||||
// Arrange - Test with server name containing special characters
|
|
||||||
when(request.getScheme()).thenReturn("https");
|
|
||||||
when(request.getServerName()).thenReturn("internal-server.example-domain.com");
|
|
||||||
when(request.getServerPort()).thenReturn(8443);
|
|
||||||
when(request.getContextPath()).thenReturn("/app-v1.2");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
String origin = UrlUtils.getOrigin(request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(
|
|
||||||
"https://internal-server.example-domain.com:8443/app-v1.2",
|
|
||||||
origin,
|
|
||||||
"Origin URL with special characters should be correctly formatted");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetOriginWithIPv4Address() {
|
|
||||||
// Arrange
|
|
||||||
when(request.getScheme()).thenReturn("http");
|
|
||||||
when(request.getServerName()).thenReturn("192.168.1.100");
|
|
||||||
when(request.getServerPort()).thenReturn(8080);
|
|
||||||
when(request.getContextPath()).thenReturn("/app");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
String origin = UrlUtils.getOrigin(request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(
|
|
||||||
"http://192.168.1.100:8080/app",
|
|
||||||
origin,
|
|
||||||
"Origin URL with IPv4 address should be correctly formatted");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetOriginWithNonStandardPort() {
|
|
||||||
// Arrange
|
|
||||||
when(request.getScheme()).thenReturn("https");
|
|
||||||
when(request.getServerName()).thenReturn("example.org");
|
|
||||||
when(request.getServerPort()).thenReturn(8443);
|
|
||||||
when(request.getContextPath()).thenReturn("/api");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
String origin = UrlUtils.getOrigin(request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(
|
|
||||||
"https://example.org:8443/api",
|
|
||||||
origin,
|
|
||||||
"Origin URL with non-standard port should be correctly formatted");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsPortAvailable() {
|
|
||||||
// We'll use a real server socket for this test
|
|
||||||
ServerSocket socket = null;
|
|
||||||
int port = 12345; // Choose a port unlikely to be in use
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First check the port is available
|
|
||||||
boolean initialAvailability = UrlUtils.isPortAvailable(port);
|
|
||||||
|
|
||||||
// Then occupy the port
|
|
||||||
socket = new ServerSocket(port);
|
|
||||||
|
|
||||||
// Now check the port is no longer available
|
|
||||||
boolean afterSocketCreation = UrlUtils.isPortAvailable(port);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertTrue(initialAvailability, "Port should be available initially");
|
|
||||||
assertFalse(
|
|
||||||
afterSocketCreation, "Port should not be available after socket is created");
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
// This might happen if the port is already in use by another process
|
|
||||||
// We'll just verify the behavior of isPortAvailable matches what we expect
|
|
||||||
assertFalse(
|
|
||||||
UrlUtils.isPortAvailable(port),
|
|
||||||
"Port should not be available if exception is thrown");
|
|
||||||
} finally {
|
|
||||||
if (socket != null && !socket.isClosed()) {
|
|
||||||
try {
|
|
||||||
socket.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Ignore cleanup exceptions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testFindAvailablePort() {
|
|
||||||
// We'll create a socket on a port and ensure findAvailablePort returns a different port
|
|
||||||
ServerSocket socket = null;
|
|
||||||
int startPort = 12346; // Choose a port unlikely to be in use
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Occupy the start port
|
|
||||||
socket = new ServerSocket(startPort);
|
|
||||||
|
|
||||||
// Find an available port
|
|
||||||
String availablePort = UrlUtils.findAvailablePort(startPort);
|
|
||||||
|
|
||||||
// Assert the returned port is not the occupied one
|
|
||||||
assertNotEquals(
|
|
||||||
String.valueOf(startPort),
|
|
||||||
availablePort,
|
|
||||||
"findAvailablePort should not return an occupied port");
|
|
||||||
|
|
||||||
// Verify the returned port is actually available
|
|
||||||
int portNumber = Integer.parseInt(availablePort);
|
|
||||||
|
|
||||||
// Close our test socket before checking the found port
|
|
||||||
socket.close();
|
|
||||||
socket = null;
|
|
||||||
|
|
||||||
// The port should now be available
|
|
||||||
assertTrue(
|
|
||||||
UrlUtils.isPortAvailable(portNumber),
|
|
||||||
"The port returned by findAvailablePort should be available");
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
// If we can't create the socket, skip this assertion
|
|
||||||
} finally {
|
|
||||||
if (socket != null && !socket.isClosed()) {
|
|
||||||
try {
|
|
||||||
socket.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Ignore cleanup exceptions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testFindAvailablePortWithAvailableStartPort() {
|
|
||||||
// Find an available port without occupying any
|
|
||||||
int startPort = 23456; // Choose a different unlikely-to-be-used port
|
|
||||||
|
|
||||||
// Make sure the port is available first
|
|
||||||
if (UrlUtils.isPortAvailable(startPort)) {
|
|
||||||
// Find an available port
|
|
||||||
String availablePort = UrlUtils.findAvailablePort(startPort);
|
|
||||||
|
|
||||||
// Assert the returned port is the start port since it's available
|
|
||||||
assertEquals(
|
|
||||||
String.valueOf(startPort),
|
|
||||||
availablePort,
|
|
||||||
"findAvailablePort should return the start port if it's available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testFindAvailablePortWithSequentialUsedPorts() {
|
|
||||||
// This test checks that findAvailablePort correctly skips multiple occupied ports
|
|
||||||
ServerSocket socket1 = null;
|
|
||||||
ServerSocket socket2 = null;
|
|
||||||
int startPort = 34567; // Another unlikely-to-be-used port
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First verify the port is available
|
|
||||||
if (!UrlUtils.isPortAvailable(startPort)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Occupy two sequential ports
|
|
||||||
socket1 = new ServerSocket(startPort);
|
|
||||||
socket2 = new ServerSocket(startPort + 1);
|
|
||||||
|
|
||||||
// Find an available port starting from our occupied range
|
|
||||||
String availablePort = UrlUtils.findAvailablePort(startPort);
|
|
||||||
int foundPort = Integer.parseInt(availablePort);
|
|
||||||
|
|
||||||
// Should have skipped the two occupied ports
|
|
||||||
assertTrue(
|
|
||||||
foundPort >= startPort + 2,
|
|
||||||
"findAvailablePort should skip sequential occupied ports");
|
|
||||||
|
|
||||||
// Verify the found port is actually available
|
|
||||||
try (ServerSocket testSocket = new ServerSocket(foundPort)) {
|
|
||||||
assertTrue(testSocket.isBound(), "The found port should be bindable");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Skip test if we encounter IO exceptions
|
|
||||||
} finally {
|
|
||||||
// Clean up resources
|
|
||||||
try {
|
|
||||||
if (socket1 != null && !socket1.isClosed()) socket1.close();
|
|
||||||
if (socket2 != null && !socket2.isClosed()) socket2.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Ignore cleanup exceptions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testIsPortAvailableWithPrivilegedPorts() {
|
|
||||||
// Skip tests for privileged ports as they typically require root access
|
|
||||||
// and results are environment-dependent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
|
||||||
|
|
||||||
public class WebResponseUtilsTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBoasToWebResponse() {
|
|
||||||
try {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
baos.write("Sample PDF content".getBytes());
|
|
||||||
String docName = "sample.pdf";
|
|
||||||
|
|
||||||
ResponseEntity<byte[]> responseEntity =
|
|
||||||
WebResponseUtils.baosToWebResponse(baos, docName);
|
|
||||||
|
|
||||||
assertNotNull(responseEntity);
|
|
||||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
|
||||||
assertNotNull(responseEntity.getBody());
|
|
||||||
|
|
||||||
HttpHeaders headers = responseEntity.getHeaders();
|
|
||||||
assertNotNull(headers);
|
|
||||||
assertEquals(MediaType.APPLICATION_PDF, headers.getContentType());
|
|
||||||
assertNotNull(headers.getContentDisposition());
|
|
||||||
// assertEquals("attachment; filename=\"sample.pdf\"",
|
|
||||||
// headers.getContentDisposition().toString());
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
fail("Exception thrown: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testMultiPartFileToWebResponse() {
|
|
||||||
try {
|
|
||||||
byte[] fileContent = "Sample file content".getBytes();
|
|
||||||
MockMultipartFile file =
|
|
||||||
new MockMultipartFile(
|
|
||||||
"file", "sample.txt", MediaType.TEXT_PLAIN_VALUE, fileContent);
|
|
||||||
|
|
||||||
ResponseEntity<byte[]> responseEntity =
|
|
||||||
WebResponseUtils.multiPartFileToWebResponse(file);
|
|
||||||
|
|
||||||
assertNotNull(responseEntity);
|
|
||||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
|
||||||
assertNotNull(responseEntity.getBody());
|
|
||||||
|
|
||||||
HttpHeaders headers = responseEntity.getHeaders();
|
|
||||||
assertNotNull(headers);
|
|
||||||
assertEquals(MediaType.TEXT_PLAIN, headers.getContentType());
|
|
||||||
assertNotNull(headers.getContentDisposition());
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
fail("Exception thrown: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBytesToWebResponse() {
|
|
||||||
try {
|
|
||||||
byte[] bytes = "Sample bytes".getBytes();
|
|
||||||
String docName = "sample.txt";
|
|
||||||
MediaType mediaType = MediaType.TEXT_PLAIN;
|
|
||||||
|
|
||||||
ResponseEntity<byte[]> responseEntity =
|
|
||||||
WebResponseUtils.bytesToWebResponse(bytes, docName, mediaType);
|
|
||||||
|
|
||||||
assertNotNull(responseEntity);
|
|
||||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
|
||||||
assertNotNull(responseEntity.getBody());
|
|
||||||
|
|
||||||
HttpHeaders headers = responseEntity.getHeaders();
|
|
||||||
assertNotNull(headers);
|
|
||||||
assertEquals(MediaType.TEXT_PLAIN, headers.getContentType());
|
|
||||||
assertNotNull(headers.getContentDisposition());
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
fail("Exception thrown: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testPdfDocToWebResponse() {
|
|
||||||
try (PDDocument document = new PDDocument()) {
|
|
||||||
document.addPage(new org.apache.pdfbox.pdmodel.PDPage());
|
|
||||||
String docName = "sample.pdf";
|
|
||||||
|
|
||||||
ResponseEntity<byte[]> responseEntity =
|
|
||||||
WebResponseUtils.pdfDocToWebResponse(document, docName);
|
|
||||||
|
|
||||||
assertNotNull(responseEntity);
|
|
||||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
|
||||||
assertNotNull(responseEntity.getBody());
|
|
||||||
|
|
||||||
HttpHeaders headers = responseEntity.getHeaders();
|
|
||||||
assertNotNull(headers);
|
|
||||||
assertEquals(MediaType.APPLICATION_PDF, headers.getContentType());
|
|
||||||
assertNotNull(headers.getContentDisposition());
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
fail("Exception thrown: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-80
@@ -1,80 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
|
||||||
import stirling.software.SPDF.service.PdfImageRemovalService;
|
|
||||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
|
||||||
import stirling.software.common.annotations.api.GeneralApi;
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
|
||||||
import stirling.software.common.util.GeneralUtils;
|
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controller class for handling PDF image removal requests. Provides an endpoint to remove images
|
|
||||||
* from a PDF file to reduce its size.
|
|
||||||
*/
|
|
||||||
@GeneralApi
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class PdfImageRemovalController {
|
|
||||||
|
|
||||||
// Service for removing images from PDFs
|
|
||||||
private final PdfImageRemovalService pdfImageRemovalService;
|
|
||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Endpoint to remove images from a PDF file.
|
|
||||||
*
|
|
||||||
* <p>This method processes the uploaded PDF file, removes all images, and returns the modified
|
|
||||||
* PDF file with a new name indicating that images were removed.
|
|
||||||
*
|
|
||||||
* @param file The PDF file with images to be removed.
|
|
||||||
* @return ResponseEntity containing the modified PDF file as byte array with appropriate
|
|
||||||
* content type and filename.
|
|
||||||
* @throws IOException If an error occurs while processing the PDF file.
|
|
||||||
*/
|
|
||||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-image-pdf")
|
|
||||||
@StandardPdfResponse
|
|
||||||
@Operation(
|
|
||||||
summary = "Remove images from file to reduce the file size.",
|
|
||||||
description =
|
|
||||||
"This endpoint remove images from file to reduce the file size.Input:PDF"
|
|
||||||
+ " Output:PDF Type:SISO")
|
|
||||||
public ResponseEntity<byte[]> removeImages(@ModelAttribute PDFFile file) throws IOException {
|
|
||||||
// Load the PDF document with proper resource management
|
|
||||||
try (PDDocument document = pdfDocumentFactory.load(file)) {
|
|
||||||
|
|
||||||
// Remove images from the PDF document using the service
|
|
||||||
try (PDDocument modifiedDocument =
|
|
||||||
pdfImageRemovalService.removeImagesFromPdf(document)) {
|
|
||||||
|
|
||||||
// Create a ByteArrayOutputStream to hold the modified PDF data
|
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
// Save the modified PDF document to the output stream
|
|
||||||
modifiedDocument.save(outputStream);
|
|
||||||
|
|
||||||
// Generate a new filename for the modified PDF
|
|
||||||
String mergedFileName =
|
|
||||||
GeneralUtils.generateFilename(
|
|
||||||
file.getFileInput().getOriginalFilename(), "_images_removed.pdf");
|
|
||||||
|
|
||||||
// Convert the byte array to a web response and return it
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
|
||||||
outputStream.toByteArray(), mergedFileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-23
@@ -150,28 +150,6 @@ public class RearrangePagesPDFController {
|
|||||||
return newPageOrder;
|
return newPageOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Rearrange pages in a PDF file by merging odd and even pages. The first half of the pages will
|
|
||||||
* be the odd pages, and the second half will be the even pages as input. <br>
|
|
||||||
* This method is visible for testing purposes only.
|
|
||||||
*
|
|
||||||
* @param totalPages Total number of pages in the PDF file.
|
|
||||||
* @return List of page numbers in the new order. The first page is 0.
|
|
||||||
*/
|
|
||||||
List<Integer> oddEvenMerge(int totalPages) {
|
|
||||||
List<Integer> newPageOrderZeroBased = new ArrayList<>();
|
|
||||||
int numberOfOddPages = (totalPages + 1) / 2;
|
|
||||||
|
|
||||||
for (int oneBasedIndex = 1; oneBasedIndex < (numberOfOddPages + 1); oneBasedIndex++) {
|
|
||||||
newPageOrderZeroBased.add((oneBasedIndex - 1));
|
|
||||||
if (numberOfOddPages + oneBasedIndex <= totalPages) {
|
|
||||||
newPageOrderZeroBased.add((numberOfOddPages + oneBasedIndex - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newPageOrderZeroBased;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Integer> duplicate(int totalPages, String pageOrder) {
|
private List<Integer> duplicate(int totalPages, String pageOrder) {
|
||||||
List<Integer> newPageOrder = new ArrayList<>();
|
List<Integer> newPageOrder = new ArrayList<>();
|
||||||
int duplicateCount;
|
int duplicateCount;
|
||||||
@@ -220,7 +198,6 @@ public class RearrangePagesPDFController {
|
|||||||
case BOOKLET_SORT -> bookletSort(totalPages);
|
case BOOKLET_SORT -> bookletSort(totalPages);
|
||||||
case SIDE_STITCH_BOOKLET_SORT -> sideStitchBooklet(totalPages);
|
case SIDE_STITCH_BOOKLET_SORT -> sideStitchBooklet(totalPages);
|
||||||
case ODD_EVEN_SPLIT -> oddEvenSplit(totalPages);
|
case ODD_EVEN_SPLIT -> oddEvenSplit(totalPages);
|
||||||
case ODD_EVEN_MERGE -> oddEvenMerge(totalPages);
|
|
||||||
case REMOVE_FIRST -> removeFirst(totalPages);
|
case REMOVE_FIRST -> removeFirst(totalPages);
|
||||||
case REMOVE_LAST -> removeLast(totalPages);
|
case REMOVE_LAST -> removeLast(totalPages);
|
||||||
case REMOVE_FIRST_AND_LAST -> removeFirstAndLast(totalPages);
|
case REMOVE_FIRST_AND_LAST -> removeFirstAndLast(totalPages);
|
||||||
|
|||||||
+63
-176
@@ -1,20 +1,14 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Image;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.RenderedImage;
|
import java.awt.image.RenderedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
import java.util.zip.Deflater;
|
import java.util.zip.Deflater;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
@@ -43,7 +37,6 @@ import stirling.software.common.annotations.api.MiscApi;
|
|||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.ExceptionUtils;
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.ImageProcessingUtils;
|
|
||||||
import stirling.software.common.util.TempFile;
|
import stirling.software.common.util.TempFile;
|
||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
@@ -65,203 +58,97 @@ public class ExtractImagesController {
|
|||||||
+ " file. Users can specify the output image format. Input:PDF"
|
+ " file. Users can specify the output image format. Input:PDF"
|
||||||
+ " Output:IMAGE/ZIP Type:SIMO")
|
+ " Output:IMAGE/ZIP Type:SIMO")
|
||||||
public ResponseEntity<StreamingResponseBody> extractImages(
|
public ResponseEntity<StreamingResponseBody> extractImages(
|
||||||
@ModelAttribute PDFExtractImagesRequest request)
|
@ModelAttribute PDFExtractImagesRequest request) throws IOException {
|
||||||
throws IOException, InterruptedException, ExecutionException {
|
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
String format = request.getFormat();
|
String imageFormat = request.getFormat();
|
||||||
boolean allowDuplicates = Boolean.TRUE.equals(request.getAllowDuplicates());
|
|
||||||
|
|
||||||
String filename = GeneralUtils.removeExtension(file.getOriginalFilename());
|
String baseFilename = GeneralUtils.removeExtension(file.getOriginalFilename());
|
||||||
Set<byte[]> processedImages = new HashSet<>();
|
Set<Integer> processedImageHashes = new HashSet<>();
|
||||||
|
|
||||||
TempFile zipTempFile = new TempFile(tempFileManager, ".zip");
|
TempFile zipFile = new TempFile(tempFileManager, ".zip");
|
||||||
try (ZipOutputStream zos =
|
try (ZipOutputStream zipStream = new ZipOutputStream(Files.newOutputStream(zipFile.getPath()));
|
||||||
new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()));
|
PDDocument pdfDoc = pdfDocumentFactory.load(file)) {
|
||||||
PDDocument document = pdfDocumentFactory.load(file)) {
|
|
||||||
|
|
||||||
// Set compression level
|
zipStream.setLevel(Deflater.BEST_COMPRESSION);
|
||||||
zos.setLevel(Deflater.BEST_COMPRESSION);
|
|
||||||
|
|
||||||
// Determine if multithreading should be used based on PDF size or number of pages
|
int totalPages = pdfDoc.getNumberOfPages();
|
||||||
boolean useMultithreading = shouldUseMultithreading(file, document);
|
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||||
|
PDPage currentPage = pdfDoc.getPage(pageIndex);
|
||||||
if (useMultithreading) {
|
extractAndAddImagesToZip(
|
||||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
currentPage, imageFormat, baseFilename, pageIndex + 1,
|
||||||
Set<Future<Void>> futures = new HashSet<>();
|
processedImageHashes, zipStream);
|
||||||
|
|
||||||
try {
|
|
||||||
int pageCount = document.getPages().getCount();
|
|
||||||
log.debug("Document reports {} pages", pageCount);
|
|
||||||
|
|
||||||
int consecutiveFailures = 0;
|
|
||||||
|
|
||||||
for (int pgNum = 0; pgNum < pageCount; pgNum++) {
|
|
||||||
try {
|
|
||||||
PDPage page = document.getPage(pgNum);
|
|
||||||
consecutiveFailures = 0; // Reset on success
|
|
||||||
final int currentPageNum =
|
|
||||||
pgNum + 1; // Convert to 1-based page numbering
|
|
||||||
Future<Void> future =
|
|
||||||
executor.submit(
|
|
||||||
() -> {
|
|
||||||
try {
|
|
||||||
// Call the image extraction method for each
|
|
||||||
// page
|
|
||||||
extractImagesFromPage(
|
|
||||||
page,
|
|
||||||
format,
|
|
||||||
filename,
|
|
||||||
currentPageNum,
|
|
||||||
processedImages,
|
|
||||||
zos,
|
|
||||||
allowDuplicates);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Log the error and continue processing other
|
|
||||||
// pages
|
|
||||||
ExceptionUtils.logException(
|
|
||||||
"image extraction from page "
|
|
||||||
+ currentPageNum,
|
|
||||||
e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // Callable requires a return type
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add the Future object to the list to track completion
|
|
||||||
futures.add(future);
|
|
||||||
} catch (Exception e) {
|
|
||||||
consecutiveFailures++;
|
|
||||||
ExceptionUtils.logException("page access for page " + (pgNum + 1), e);
|
|
||||||
|
|
||||||
if (consecutiveFailures >= 3) {
|
|
||||||
log.warn("Stopping page iteration after 3 consecutive failures");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ExceptionUtils.logException("page count determination", e);
|
zipFile.close();
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all tasks to complete
|
|
||||||
for (Future<Void> future : futures) {
|
|
||||||
future.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close executor service
|
|
||||||
executor.shutdown();
|
|
||||||
} else {
|
|
||||||
// Single-threaded extraction
|
|
||||||
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
|
||||||
PDPage page = document.getPage(pgNum);
|
|
||||||
extractImagesFromPage(
|
|
||||||
page,
|
|
||||||
format,
|
|
||||||
filename,
|
|
||||||
pgNum + 1,
|
|
||||||
processedImages,
|
|
||||||
zos,
|
|
||||||
allowDuplicates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// document and zos closed by try-with-resources
|
|
||||||
} catch (Exception e) {
|
|
||||||
zipTempFile.close();
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebResponseUtils.zipFileToWebResponse(
|
return WebResponseUtils.zipFileToWebResponse(
|
||||||
zipTempFile, filename + "_extracted-images.zip");
|
zipFile, baseFilename + "_extracted-images.zip");
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldUseMultithreading(MultipartFile file, PDDocument document) {
|
private void extractAndAddImagesToZip(
|
||||||
// Criteria: Use multithreading if file size > 10MB or number of pages > 20
|
|
||||||
long fileSizeInMB = file.getSize() / (1024 * 1024);
|
|
||||||
int numberOfPages = document.getPages().getCount();
|
|
||||||
return fileSizeInMB > 10 || numberOfPages > 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void extractImagesFromPage(
|
|
||||||
PDPage page,
|
PDPage page,
|
||||||
String format,
|
String imageFormat,
|
||||||
String filename,
|
String baseFilename,
|
||||||
int pageNum,
|
int pageNumber,
|
||||||
Set<byte[]> processedImages,
|
Set<Integer> seenImageHashes,
|
||||||
ZipOutputStream zos,
|
ZipOutputStream zipOutput)
|
||||||
boolean allowDuplicates)
|
|
||||||
throws IOException {
|
throws IOException {
|
||||||
MessageDigest md;
|
|
||||||
try {
|
|
||||||
md = MessageDigest.getInstance("MD5");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
log.error("MD5 algorithm not available for extractImages hash.", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (page.getResources() == null || page.getResources().getXObjectNames() == null) {
|
if (page.getResources() == null || page.getResources().getXObjectNames() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int count = 1;
|
|
||||||
for (COSName name : page.getResources().getXObjectNames()) {
|
int imageCount = 1;
|
||||||
|
for (COSName resourceName : page.getResources().getXObjectNames()) {
|
||||||
|
if (!page.getResources().isImageXObject(resourceName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (page.getResources().isImageXObject(name)) {
|
PDImageXObject imageObject =
|
||||||
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
(PDImageXObject) page.getResources().getXObject(resourceName);
|
||||||
if (!allowDuplicates) {
|
int imageHashCode = imageObject.hashCode();
|
||||||
byte[] data = ImageProcessingUtils.getImageData(image.getImage());
|
|
||||||
byte[] imageHash = md.digest(data);
|
|
||||||
synchronized (processedImages) {
|
|
||||||
if (processedImages.stream()
|
|
||||||
.anyMatch(hash -> Arrays.equals(hash, imageHash))) {
|
|
||||||
continue; // Skip already processed images
|
|
||||||
}
|
|
||||||
processedImages.add(imageHash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderedImage renderedImage = image.getImage();
|
if (seenImageHashes.contains(imageHashCode)) {
|
||||||
|
continue;
|
||||||
// Convert to standard RGB colorspace if needed
|
|
||||||
BufferedImage bufferedImage = convertToRGB(renderedImage, format);
|
|
||||||
|
|
||||||
// Encode image outside the lock to allow parallel encoding across threads
|
|
||||||
String imageName = filename + "_page_" + pageNum + "_" + count++ + "." + format;
|
|
||||||
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(bufferedImage, format, imageBaos);
|
|
||||||
byte[] imageData = imageBaos.toByteArray();
|
|
||||||
|
|
||||||
// Write encoded bytes to zip under lock (ZipOutputStream requires
|
|
||||||
// serialization)
|
|
||||||
synchronized (zos) {
|
|
||||||
zos.putNextEntry(new ZipEntry(imageName));
|
|
||||||
zos.write(imageData);
|
|
||||||
zos.closeEntry();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
seenImageHashes.add(imageHashCode);
|
||||||
|
|
||||||
|
RenderedImage sourceImage = imageObject.getImage();
|
||||||
|
BufferedImage convertedImage = convertImageToFormat(sourceImage, imageFormat);
|
||||||
|
|
||||||
|
String imagePath =
|
||||||
|
baseFilename + "_page_" + pageNumber + "_" + imageCount++ + "."
|
||||||
|
+ imageFormat;
|
||||||
|
ByteArrayOutputStream imageBuffer = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(convertedImage, imageFormat, imageBuffer);
|
||||||
|
|
||||||
|
zipOutput.putNextEntry(new ZipEntry(imagePath));
|
||||||
|
zipOutput.write(imageBuffer.toByteArray());
|
||||||
|
zipOutput.closeEntry();
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ExceptionUtils.logException("image extraction", e);
|
ExceptionUtils.logException("image extraction failed", e);
|
||||||
throw ExceptionUtils.handlePdfException(e, "during image extraction");
|
throw ExceptionUtils.handlePdfException(e, "during image extraction");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private BufferedImage convertToRGB(RenderedImage renderedImage, String format) {
|
private BufferedImage convertImageToFormat(RenderedImage source, String format) {
|
||||||
int width = renderedImage.getWidth();
|
int width = source.getWidth();
|
||||||
int height = renderedImage.getHeight();
|
int height = source.getHeight();
|
||||||
BufferedImage rgbImage;
|
|
||||||
|
|
||||||
|
int imageType = BufferedImage.TYPE_INT_RGB;
|
||||||
if ("png".equalsIgnoreCase(format)) {
|
if ("png".equalsIgnoreCase(format)) {
|
||||||
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
imageType = BufferedImage.TYPE_INT_ARGB;
|
||||||
} else if ("jpeg".equalsIgnoreCase(format) || "jpg".equalsIgnoreCase(format)) {
|
|
||||||
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
|
||||||
} else if ("gif".equalsIgnoreCase(format)) {
|
|
||||||
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
|
|
||||||
} else {
|
|
||||||
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Graphics2D g = rgbImage.createGraphics();
|
BufferedImage result = new BufferedImage(width, height, imageType);
|
||||||
g.drawImage((Image) renderedImage, 0, 0, null);
|
Graphics2D graphics = result.createGraphics();
|
||||||
g.dispose();
|
graphics.drawImage((Image) source, 0, 0, null);
|
||||||
return rgbImage;
|
graphics.dispose();
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-16
@@ -36,7 +36,6 @@ import stirling.software.SPDF.model.PipelineResult;
|
|||||||
import stirling.software.SPDF.service.ApiDocService;
|
import stirling.software.SPDF.service.ApiDocService;
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.service.PostHogService;
|
import stirling.software.common.service.PostHogService;
|
||||||
import stirling.software.common.util.FileMonitor;
|
|
||||||
|
|
||||||
import tools.jackson.databind.ObjectMapper;
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@@ -50,7 +49,6 @@ public class PipelineDirectoryProcessor {
|
|||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final ApiDocService apiDocService;
|
private final ApiDocService apiDocService;
|
||||||
private final PipelineProcessor processor;
|
private final PipelineProcessor processor;
|
||||||
private final FileMonitor fileMonitor;
|
|
||||||
private final PostHogService postHogService;
|
private final PostHogService postHogService;
|
||||||
private final List<String> watchedFoldersDirs;
|
private final List<String> watchedFoldersDirs;
|
||||||
private final String finishedFoldersDir;
|
private final String finishedFoldersDir;
|
||||||
@@ -63,13 +61,11 @@ public class PipelineDirectoryProcessor {
|
|||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
ApiDocService apiDocService,
|
ApiDocService apiDocService,
|
||||||
PipelineProcessor processor,
|
PipelineProcessor processor,
|
||||||
FileMonitor fileMonitor,
|
|
||||||
PostHogService postHogService,
|
PostHogService postHogService,
|
||||||
RuntimePathConfig runtimePathConfig) {
|
RuntimePathConfig runtimePathConfig) {
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.apiDocService = apiDocService;
|
this.apiDocService = apiDocService;
|
||||||
this.processor = processor;
|
this.processor = processor;
|
||||||
this.fileMonitor = fileMonitor;
|
|
||||||
this.postHogService = postHogService;
|
this.postHogService = postHogService;
|
||||||
this.watchedFoldersDirs = runtimePathConfig.getPipelineWatchedFoldersPaths();
|
this.watchedFoldersDirs = runtimePathConfig.getPipelineWatchedFoldersPaths();
|
||||||
this.finishedFoldersDir = runtimePathConfig.getPipelineFinishedFoldersPath();
|
this.finishedFoldersDir = runtimePathConfig.getPipelineFinishedFoldersPath();
|
||||||
@@ -274,18 +270,7 @@ public class PipelineDirectoryProcessor {
|
|||||||
return isAllowed;
|
return isAllowed;
|
||||||
})
|
})
|
||||||
.map(Path::toAbsolutePath)
|
.map(Path::toAbsolutePath)
|
||||||
.filter(
|
.filter(path -> true)
|
||||||
path -> {
|
|
||||||
boolean isReady =
|
|
||||||
fileMonitor.isFileReadyForProcessing(path);
|
|
||||||
if (!isReady) {
|
|
||||||
log.info(
|
|
||||||
"File not ready for processing (locked/created"
|
|
||||||
+ " last 5s): {}",
|
|
||||||
path);
|
|
||||||
}
|
|
||||||
return isReady;
|
|
||||||
})
|
|
||||||
.map(Path::toFile)
|
.map(Path::toFile)
|
||||||
.toArray(File[]::new);
|
.toArray(File[]::new);
|
||||||
log.info(
|
log.info(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ public enum SortTypes {
|
|||||||
BOOKLET_SORT,
|
BOOKLET_SORT,
|
||||||
SIDE_STITCH_BOOKLET_SORT,
|
SIDE_STITCH_BOOKLET_SORT,
|
||||||
ODD_EVEN_SPLIT,
|
ODD_EVEN_SPLIT,
|
||||||
ODD_EVEN_MERGE,
|
|
||||||
REMOVE_FIRST,
|
REMOVE_FIRST,
|
||||||
REMOVE_LAST,
|
REMOVE_LAST,
|
||||||
REMOVE_FIRST_AND_LAST,
|
REMOVE_FIRST_AND_LAST,
|
||||||
|
|||||||
-1
@@ -22,7 +22,6 @@ public class RearrangePagesRequest extends PDFWithPageNums {
|
|||||||
+ "DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). "
|
+ "DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). "
|
||||||
+ "BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\n"
|
+ "BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\n"
|
||||||
+ "ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\n"
|
+ "ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\n"
|
||||||
+ "ODD_EVEN_MERGE: Merges pages and organises them alternately into odd and even pages.\n"
|
|
||||||
+ "REMOVE_FIRST: Removes the first page.\n"
|
+ "REMOVE_FIRST: Removes the first page.\n"
|
||||||
+ "REMOVE_LAST: Removes the last page.\n"
|
+ "REMOVE_LAST: Removes the last page.\n"
|
||||||
+ "REMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n")
|
+ "REMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n")
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
package stirling.software.SPDF.service;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.cos.COSName;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDResources;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
/** Service class responsible for removing image objects from a PDF document. */
|
|
||||||
@Service
|
|
||||||
public class PdfImageRemovalService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes all image objects from the provided PDF document.
|
|
||||||
*
|
|
||||||
* <p>This method iterates over each page in the document and removes any image XObjects found
|
|
||||||
* in the page's resources.
|
|
||||||
*
|
|
||||||
* @param document The PDF document from which images will be removed.
|
|
||||||
* @return The modified PDF document with images removed.
|
|
||||||
* @throws IOException If an error occurs while processing the PDF document.
|
|
||||||
*/
|
|
||||||
public PDDocument removeImagesFromPdf(PDDocument document) throws IOException {
|
|
||||||
// Iterate over each page in the PDF document
|
|
||||||
for (PDPage page : document.getPages()) {
|
|
||||||
PDResources resources = page.getResources();
|
|
||||||
// Collect the XObject names to remove
|
|
||||||
List<COSName> namesToRemove = new ArrayList<>();
|
|
||||||
|
|
||||||
// Iterate over all XObject names in the page's resources
|
|
||||||
for (COSName name : resources.getXObjectNames()) {
|
|
||||||
// Check if the XObject is an image
|
|
||||||
if (resources.isImageXObject(name)) {
|
|
||||||
// Collect the name for removal
|
|
||||||
namesToRemove.add(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, modify the resources by removing the collected names
|
|
||||||
for (COSName name : namesToRemove) {
|
|
||||||
resources.put(name, (PDXObject) null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
#searchBar {
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
background-color: var(--md-sys-color-surface-container-low);
|
|
||||||
width: 100%;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding: 0.75rem 3.5rem;
|
|
||||||
border: 1px solid var(--md-sys-color-outline-variant);
|
|
||||||
border-radius: 3rem;
|
|
||||||
outline-color: var(--md-sys-color-outline-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
#filtersContainer {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-button {
|
|
||||||
color: var(--md-sys-color-secondary);
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.3s;
|
|
||||||
transform-origin: center center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-button:hover {
|
|
||||||
transform: scale(1.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
position: absolute;
|
|
||||||
margin: 0.75rem 1rem;
|
|
||||||
border: 0.1rem solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorite-icon {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
color: var(--md-sys-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
#tool-icon {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tool-text {
|
|
||||||
margin: 0.0rem 0 0 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorite-icon img {
|
|
||||||
filter: brightness(0) invert(var(--md-theme-filter-color));
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorite-icon:hover .material-symbols-rounded {
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorite-icon .material-symbols-rounded.fill{
|
|
||||||
color: #f5c000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jumbotron {
|
|
||||||
padding: 3rem 3rem;
|
|
||||||
/* Reduce vertical padding */
|
|
||||||
}
|
|
||||||
|
|
||||||
.lookatme {
|
|
||||||
opacity: 1;
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lookatme::after {
|
|
||||||
color: #e33100;
|
|
||||||
text-shadow: 0 0 5px #e33100;
|
|
||||||
/* in the html, the data-lookatme-text attribute must */
|
|
||||||
/* contain the same text as the .lookatme element */
|
|
||||||
content: attr(data-lookatme-text);
|
|
||||||
padding: inherit;
|
|
||||||
position: absolute;
|
|
||||||
inset: 0 0 0 0;
|
|
||||||
z-index: 1;
|
|
||||||
/* 20 steps / 2 seconds = 10fps */
|
|
||||||
-webkit-animation: 2s infinite Pulse steps(20);
|
|
||||||
animation: 2s infinite Pulse steps(20);
|
|
||||||
}
|
|
||||||
|
|
||||||
.newfeature{
|
|
||||||
display: flex;
|
|
||||||
width:fit-content
|
|
||||||
}
|
|
||||||
.recent-features{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-icon {
|
|
||||||
color: var(--favourite-remove) !important;
|
|
||||||
}
|
|
||||||
.add-icon {
|
|
||||||
color: var(--favourite-add) !important;
|
|
||||||
}
|
|
||||||
@@ -1,625 +0,0 @@
|
|||||||
/* Base Container Styles */
|
|
||||||
.multi-tool-container {
|
|
||||||
max-width: 100vw;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Elements */
|
|
||||||
label {
|
|
||||||
text-align: left;
|
|
||||||
display: block;
|
|
||||||
padding: 0rem 0.25rem;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
border-radius: 16px !important;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border: 1px solid var(--theme-color-outline-variant);
|
|
||||||
flex-grow: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Action Bar Styles */
|
|
||||||
.mt-action-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: start;
|
|
||||||
border: none;
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
top: 10px;
|
|
||||||
z-index: 11;
|
|
||||||
padding: 1.25rem;
|
|
||||||
border-radius: 2rem;
|
|
||||||
margin: 0px 25px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-bar > * {
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-file-uploader {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-bar svg,
|
|
||||||
.mt-action-btn svg {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-bar .mt-filename {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Action Button Styles */
|
|
||||||
.mt-action-btn {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 80px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
border-radius: 2rem;
|
|
||||||
z-index: 12;
|
|
||||||
background-color: var(--md-sys-color-surface-container-low);
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
width: fit-content;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 10px 20px;
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn .btn {
|
|
||||||
width: 3.5rem;
|
|
||||||
height: 3.5rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card and Container Styles */
|
|
||||||
.bg-card {
|
|
||||||
background-color: var(--md-sys-color-surface-5);
|
|
||||||
border-radius: 3rem;
|
|
||||||
padding: 15px 0;
|
|
||||||
margin-left: 55px;
|
|
||||||
margin-right: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pages-container-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 25px;
|
|
||||||
min-height: 275px;
|
|
||||||
margin: 0 0 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pages-container {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 20px;
|
|
||||||
justify-content: flex-start;
|
|
||||||
width: fit-content;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar Styles */
|
|
||||||
#pages-container-wrapper::-webkit-scrollbar {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pages-container-wrapper::-webkit-scrollbar-track {
|
|
||||||
background: var(--scroll-bar-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#pages-container-wrapper::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--scroll-bar-thumb);
|
|
||||||
}
|
|
||||||
|
|
||||||
#pages-container-wrapper::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--scroll-bar-thumb-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Page Container Base Styles */
|
|
||||||
.page-container {
|
|
||||||
display: inline-block;
|
|
||||||
list-style-type: none;
|
|
||||||
width: 260px;
|
|
||||||
height: 260px;
|
|
||||||
line-height: 50px;
|
|
||||||
margin-top: 15px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
text-align: center;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
|
||||||
transition: width 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Page Container Sizes */
|
|
||||||
@media only screen and (max-width: 480px) {
|
|
||||||
.page-container {
|
|
||||||
width: calc(100vw - 90px);
|
|
||||||
height: calc(100vw - 90px);
|
|
||||||
max-width: 300px;
|
|
||||||
max-height: 300px;
|
|
||||||
margin: 5px auto;
|
|
||||||
}
|
|
||||||
#pages-container { gap: 10px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 481px) and (max-width: 768px) {
|
|
||||||
.page-container {
|
|
||||||
width: calc((100vw - 120px - 12px) / 2);
|
|
||||||
height: calc((100vw - 120px - 12px) / 2);
|
|
||||||
max-width: 250px;
|
|
||||||
max-height: 250px;
|
|
||||||
margin: 6px;
|
|
||||||
}
|
|
||||||
#pages-container { gap: 12px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 769px) and (max-width: 1199px) {
|
|
||||||
.page-container {
|
|
||||||
width: calc((100vw - 140px - 45px) / 3);
|
|
||||||
height: calc((100vw - 140px - 45px) / 3);
|
|
||||||
max-width: 220px;
|
|
||||||
max-height: 220px;
|
|
||||||
margin: 7px;
|
|
||||||
}
|
|
||||||
#pages-container { gap: 15px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 1200px) and (max-width: 1280px) {
|
|
||||||
.page-container {
|
|
||||||
width: calc((100vw - 160px - 60px) / 4);
|
|
||||||
height: calc((100vw - 160px - 60px) / 4);
|
|
||||||
max-width: 200px;
|
|
||||||
max-height: 200px;
|
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
#pages-container { gap: 17px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 1281px) {
|
|
||||||
.page-container {
|
|
||||||
width: calc((100vw - 180px - 80px) / 5);
|
|
||||||
height: calc((100vw - 180px - 80px) / 5);
|
|
||||||
max-width: 190px;
|
|
||||||
max-height: 190px;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
#pages-container { gap: 20px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Split Page Styles */
|
|
||||||
.page-container.split-before {
|
|
||||||
border-left: 1px dashed var(--md-sys-color-on-surface);
|
|
||||||
padding-left: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container.split-before:first-child {
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container:first-child .pdf-actions_split-file-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RTL Language Support */
|
|
||||||
.page-container:last-child:lang(ar),
|
|
||||||
.page-container:last-child:lang(he),
|
|
||||||
.page-container:last-child:lang(fa),
|
|
||||||
.page-container:last-child:lang(ur),
|
|
||||||
.page-container:last-child:lang(ckb),
|
|
||||||
.page-container:last-child:lang(ks),
|
|
||||||
.page-container:last-child:lang(kk),
|
|
||||||
.page-container:last-child:lang(uz),
|
|
||||||
.page-container:last-child:lang(ky),
|
|
||||||
.page-container:last-child:lang(bal),
|
|
||||||
.page-container:last-child:lang(dv),
|
|
||||||
.page-container:last-child:lang(ps),
|
|
||||||
.page-container:last-child:lang(sdg),
|
|
||||||
.page-container:last-child:lang(syr),
|
|
||||||
.page-container:last-child:lang(mzn),
|
|
||||||
.page-container:last-child:lang(tgl),
|
|
||||||
.page-container:last-child:lang(pnb),
|
|
||||||
.page-container:last-child:lang(ug),
|
|
||||||
.page-container:last-child:lang(nqo),
|
|
||||||
.page-container:last-child:lang(bqi) {
|
|
||||||
margin-left: auto !important;
|
|
||||||
margin-right: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Page Image Styles */
|
|
||||||
.page-container img {
|
|
||||||
max-width: calc(100% - 8px);
|
|
||||||
max-height: calc(100% - 8px);
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
translate: -50% -50%;
|
|
||||||
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: rotate 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Page Number Styles */
|
|
||||||
.page-number {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
left: 5px;
|
|
||||||
line-height: normal;
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
background-color: rgba(162, 201, 255, 0.8);
|
|
||||||
padding: 6px 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
z-index: 2;
|
|
||||||
font-weight: 450;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tool Header and Button Styles */
|
|
||||||
.tool-header {
|
|
||||||
margin: 0.5rem 1rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#select-pages-button {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
#add-pdf-button {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Selected Pages Container Styles */
|
|
||||||
.selected-pages-container {
|
|
||||||
background-color: var(--md-sys-color-surface);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 15px;
|
|
||||||
width: 100%;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-pages-container h3 {
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
font-size: 1.2em;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-pages-header {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-pages-header h5 {
|
|
||||||
margin: 0 0 8px 0 !important;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-pages-header #csv-input {
|
|
||||||
width: 100%;
|
|
||||||
height: 2.5rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
|
||||||
background-color: var(--md-sys-color-surface);
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-pages-header #csv-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
box-shadow: 0 0 0 2px rgba(var(--md-sys-color-primary-rgb), 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pages List Styles */
|
|
||||||
.pages-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
min-height: 0;
|
|
||||||
max-height: 10rem;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-item {
|
|
||||||
background-color: var(--md-sys-color-surface-container-low);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
width: 7rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-page-number {
|
|
||||||
width: 4rem;
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-btn {
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-container {
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label {
|
|
||||||
font-size: medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Zoom Level Responsive Styles */
|
|
||||||
@media only screen and (min-width: 2000px) {
|
|
||||||
.mt-action-btn {
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: 2.5rem;
|
|
||||||
}
|
|
||||||
.mt-action-btn .btn {
|
|
||||||
width: 4rem;
|
|
||||||
height: 4rem;
|
|
||||||
border-radius: 22px;
|
|
||||||
}
|
|
||||||
.mt-action-btn .btn .material-symbols-rounded {
|
|
||||||
font-size: 1.7rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 2560px) {
|
|
||||||
.mt-action-btn {
|
|
||||||
gap: 15px;
|
|
||||||
padding: 15px 30px;
|
|
||||||
border-radius: 3rem;
|
|
||||||
}
|
|
||||||
.mt-action-btn .btn {
|
|
||||||
width: 5rem;
|
|
||||||
height: 5rem;
|
|
||||||
border-radius: 28px;
|
|
||||||
}
|
|
||||||
.mt-action-btn .btn .material-symbols-rounded {
|
|
||||||
font-size: 2.1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 3840px) {
|
|
||||||
.mt-action-btn {
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px 40px;
|
|
||||||
border-radius: 4rem;
|
|
||||||
}
|
|
||||||
.mt-action-btn .btn {
|
|
||||||
width: 7rem;
|
|
||||||
height: 7rem;
|
|
||||||
border-radius: 40px;
|
|
||||||
}
|
|
||||||
.mt-action-btn .btn .material-symbols-rounded {
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 7680px) {
|
|
||||||
.mt-action-btn {
|
|
||||||
gap: 40px;
|
|
||||||
padding: 40px 80px;
|
|
||||||
border-radius: 8rem;
|
|
||||||
}
|
|
||||||
.mt-action-btn .btn {
|
|
||||||
width: 14rem;
|
|
||||||
height: 14rem;
|
|
||||||
border-radius: 80px;
|
|
||||||
}
|
|
||||||
.mt-action-btn .btn .material-symbols-rounded {
|
|
||||||
font-size: 6rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Zoom-responsive Sidebar Styles */
|
|
||||||
@media only screen and (max-width: 1280px) {
|
|
||||||
.container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn {
|
|
||||||
position: fixed !important;
|
|
||||||
left: 10px !important;
|
|
||||||
top: 80px !important;
|
|
||||||
bottom: 20px !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
border-radius: 16px !important;
|
|
||||||
background-color: var(--md-sys-color-surface-container-low);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: column !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
height: calc(100vh - 100px) !important;
|
|
||||||
overflow-y: auto !important;
|
|
||||||
scrollbar-width: none !important;
|
|
||||||
-ms-overflow-style: none !important;
|
|
||||||
z-index: 12;
|
|
||||||
width: 70px !important;
|
|
||||||
gap: 8px !important;
|
|
||||||
padding: 12px 8px !important;
|
|
||||||
box-sizing: border-box !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn:has(.btn:nth-last-child(n+7)) {
|
|
||||||
justify-content: flex-start !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn::-webkit-scrollbar {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn .btn {
|
|
||||||
width: 48px !important;
|
|
||||||
height: 48px !important;
|
|
||||||
border-radius: 12px !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 16px !important;
|
|
||||||
box-sizing: border-box !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn .btn .material-symbols-rounded {
|
|
||||||
font-size: 20px !important;
|
|
||||||
line-height: 1 !important;
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multi-tool-container {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-bar {
|
|
||||||
margin-left: 25px !important;
|
|
||||||
margin-right: 25px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn:not(:has(*:nth-child(6))) {
|
|
||||||
justify-content: center !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile and High Zoom Responsive Styles */
|
|
||||||
@media only screen and (max-width: 960px) {
|
|
||||||
.mt-action-btn {
|
|
||||||
left: 8px !important;
|
|
||||||
top: 75px !important;
|
|
||||||
bottom: 15px !important;
|
|
||||||
height: calc(100vh - 90px) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
|
||||||
.mt-action-btn {
|
|
||||||
left: 5px !important;
|
|
||||||
width: 60px !important;
|
|
||||||
top: calc(var(--navbar-height, 60px) + 10px) !important;
|
|
||||||
bottom: 10px !important;
|
|
||||||
height: calc(100vh - var(--navbar-height, 60px) - 20px) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn .btn {
|
|
||||||
width: 40px !important;
|
|
||||||
height: 40px !important;
|
|
||||||
box-sizing: border-box !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn .btn .material-symbols-rounded {
|
|
||||||
font-size: 16px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pages-container {
|
|
||||||
margin: 0 auto !important;
|
|
||||||
display: flex !important;
|
|
||||||
flex-wrap: wrap !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container {
|
|
||||||
width: calc(100vw - 55px) !important;
|
|
||||||
height: calc(100vw - 60px) !important;
|
|
||||||
max-width: 350px !important;
|
|
||||||
max-height: 350px !important;
|
|
||||||
margin: 5px auto !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
|
||||||
.mt-action-btn {
|
|
||||||
left: 2px !important;
|
|
||||||
top: calc(var(--navbar-height, 60px) + 5px) !important;
|
|
||||||
bottom: 5px !important;
|
|
||||||
height: calc(100vh - var(--navbar-height, 60px) - 10px) !important;
|
|
||||||
width: 50px !important;
|
|
||||||
gap: 6px !important;
|
|
||||||
padding: 10px 6px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn .btn {
|
|
||||||
width: 32px !important;
|
|
||||||
height: 32px !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
box-sizing: border-box !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn .btn .material-symbols-rounded {
|
|
||||||
font-size: 14px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 384px) {
|
|
||||||
.mt-action-btn {
|
|
||||||
left: 2px !important;
|
|
||||||
top: calc(var(--navbar-height, 60px) + 5px) !important;
|
|
||||||
bottom: 5px !important;
|
|
||||||
height: calc(100vh - var(--navbar-height, 60px) - 10px) !important;
|
|
||||||
width: 40px !important;
|
|
||||||
gap: 4px !important;
|
|
||||||
padding: 8px 4px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn .btn {
|
|
||||||
width: 28px !important;
|
|
||||||
height: 28px !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
box-sizing: border-box !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-action-btn .btn .material-symbols-rounded {
|
|
||||||
font-size: 12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container {
|
|
||||||
width: calc(100vw - 55px) !important;
|
|
||||||
height: calc(100vw - 60px) !important;
|
|
||||||
max-width: 280px !important;
|
|
||||||
max-height: 280px !important;
|
|
||||||
margin: 3px auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container img {
|
|
||||||
max-width: calc(100% - 4px) !important;
|
|
||||||
max-height: calc(100% - 4px) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,799 +0,0 @@
|
|||||||
#navbarSearch {
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
transition: all 0.3s;
|
|
||||||
max-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#navbarSearch.show {
|
|
||||||
height: auto;
|
|
||||||
/*dynamically changes height*/
|
|
||||||
}
|
|
||||||
|
|
||||||
#searchResults .dropdown-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
height: 50px;
|
|
||||||
/* Fixed height */
|
|
||||||
overflow: hidden;
|
|
||||||
/* Hide overflow */
|
|
||||||
}
|
|
||||||
|
|
||||||
#searchResults .icon {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#searchResults .icon-text {
|
|
||||||
display: inline;
|
|
||||||
overflow: hidden;
|
|
||||||
/* Hide overflow */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
/* Add ellipsis for long text */
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-icon i {
|
|
||||||
font-size: 24px;
|
|
||||||
/* Adjust this to your desired size */
|
|
||||||
transition: color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-icon:hover i {
|
|
||||||
color: #666;
|
|
||||||
/* Adjust this to your hover color */
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
transition: border 0.3s, box-shadow 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input:focus {
|
|
||||||
border-color: #666;
|
|
||||||
/* Adjust this to your focus color */
|
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
||||||
/* Adjust this to your desired shadow */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.main-icon {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
vertical-align: middle;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive navbar brand - prevent hamburger from wrapping */
|
|
||||||
.navbar-brand {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 0; /* Allow shrinking */
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand .icon-text {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Progressive text shrinking to prevent hamburger wrap */
|
|
||||||
@media only screen and (max-width: 480px) {
|
|
||||||
.navbar-brand .icon-text {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-icon {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 420px) {
|
|
||||||
.navbar-brand .icon-text {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-icon {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 380px) {
|
|
||||||
.navbar-brand .icon-text {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 340px) {
|
|
||||||
.navbar-brand .icon-text {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ultra-small screens - hide text, keep icon */
|
|
||||||
@media only screen and (max-width: 320px) {
|
|
||||||
.navbar-brand .icon-text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon {
|
|
||||||
vertical-align: middle;
|
|
||||||
font-size: 2rem !important;
|
|
||||||
padding: 0.25rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon+.icon {
|
|
||||||
margin-left: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-text {
|
|
||||||
margin-left: 8px;
|
|
||||||
margin-right: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.scalable-languages-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(1, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 400px) {
|
|
||||||
#languageSelection.scalable-languages-container {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 600px) {
|
|
||||||
#languageSelection.scalable-languages-container {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
#languageSelection.scalable-languages-container {
|
|
||||||
grid-template-columns: repeat(4, 1fr) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scalable-languages-container:not(:has(> :nth-child(4))) .lang-dropdown-item-wrapper:last-child {
|
|
||||||
border: 0px !important
|
|
||||||
}
|
|
||||||
|
|
||||||
html[dir="ltr"] #languageSelection .lang-dropdown-item-wrapper:last-child {
|
|
||||||
border-right: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#languageSelection .lang-dropdown-item-wrapper:last-child {
|
|
||||||
border: 0px !important;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.scalable-languages-container {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scalable-languages-container:not(:has(> :nth-child(2))) {
|
|
||||||
grid-template-columns: repeat(var(--count), 1fr) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scalable-languages-container .lang-dropdown-item-wrapper:nth-child(2n) {
|
|
||||||
border: 0px
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 601px) and (max-width: 900px) {
|
|
||||||
.scalable-languages-container {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scalable-languages-container:not(:has(> :nth-child(3))) {
|
|
||||||
grid-template-columns: repeat(var(--count), 1fr) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scalable-languages-container .lang-dropdown-item-wrapper:nth-child(3n) {
|
|
||||||
border: 0px
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 901px) {
|
|
||||||
.scalable-languages-container {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scalable-languages-container:not(:has(> :nth-child(4))) {
|
|
||||||
grid-template-columns: repeat(var(--count), 1fr) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scalable-languages-container .lang-dropdown-item-wrapper:nth-child(4n) {
|
|
||||||
border: 0px
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scalable-languages-container:has(> *:nth-child(1)) {
|
|
||||||
--count: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scalable-languages-container:has(> *:nth-child(2)) {
|
|
||||||
--count: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scalable-languages-container:has(> *:nth-child(3)) {
|
|
||||||
--count: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[dir="ltr"] .lang-dropdown-item-wrapper {
|
|
||||||
border-right: 2px solid var(--md-nav-color-on-separator);
|
|
||||||
}
|
|
||||||
|
|
||||||
html[dir="rtl"] .lang-dropdown-item-wrapper {
|
|
||||||
border-left: 2px solid var(--md-nav-color-on-separator);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-lock-y {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 30vh;
|
|
||||||
overscroll-behavior-y: contain;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.lang-dropdown-item-wrapper .dropdown-item {
|
|
||||||
min-width: 200px
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-lock-y {
|
|
||||||
max-height: 80vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item .icon-text {
|
|
||||||
text-wrap: wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
width: 80%;
|
|
||||||
font-size: large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-icon {
|
|
||||||
color: var(--md-sys-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-icon:hover {
|
|
||||||
transform: scale(1.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
span.icon-text::after {
|
|
||||||
content: attr(data-text);
|
|
||||||
content: attr(data-text) / "";
|
|
||||||
font-weight: 600;
|
|
||||||
height: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
overflow: hidden;
|
|
||||||
user-select: none;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item-separator {
|
|
||||||
position: relative;
|
|
||||||
margin: 0 4px;
|
|
||||||
/* Adjust the margin as needed */
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item-separator::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 10%;
|
|
||||||
/* Adjust the top and bottom margins as needed */
|
|
||||||
bottom: 10%;
|
|
||||||
width: 1px;
|
|
||||||
background-color: #ccc;
|
|
||||||
/* Adjust the color as needed */
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-toggler {
|
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 98vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chevron-icon {
|
|
||||||
margin-left: auto;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
[aria-expanded="true"] > .chevron-icon {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .tooltip-text {
|
|
||||||
visibility: hidden;
|
|
||||||
background-color: rgb(71 67 67 / 80%);
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 5px;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
top: 100%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
font-size: 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item:hover .tooltip-text {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.dropdown-menu.scrollable-y {
|
|
||||||
overflow-y: scroll;
|
|
||||||
max-height: 360px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dropdown Scrollbar*/
|
|
||||||
.scrollable-y {
|
|
||||||
overflow-y: scroll;
|
|
||||||
max-height: 190px;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable-y::-webkit-scrollbar {
|
|
||||||
background: transparent;
|
|
||||||
width: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable-y::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable-y::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 2rem;
|
|
||||||
background-color: var(--md-sys-color-surface-5);
|
|
||||||
border: 3px solid var(--md-sys-color-surface-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mega Menu */
|
|
||||||
.dropdown-mega .dropdown-menu {
|
|
||||||
width: 100%;
|
|
||||||
left: 0 !important;
|
|
||||||
right: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-mega .title {
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu .list-group {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-content .dropdown-item:focus .nav-icon,
|
|
||||||
.mega-content .dropdown-item:hover .nav-icon,
|
|
||||||
.mega-content .dropdown-item.active .nav-icon {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:focus.sign,
|
|
||||||
.dropdown-item:hover.sign,
|
|
||||||
.dropdown-item.active.sign {
|
|
||||||
color: var(--md-nav-on-section-color-sign);
|
|
||||||
background-color: var(--md-nav-section-color-sign);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:focus.organize,
|
|
||||||
.dropdown-item:hover.organize,
|
|
||||||
.dropdown-item.active.organize {
|
|
||||||
color: var(--md-nav-on-section-color-organize);
|
|
||||||
background-color: var(--md-nav-section-color-organize);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:focus.convert,
|
|
||||||
.dropdown-item:hover.convert,
|
|
||||||
.dropdown-item.active.convert {
|
|
||||||
color: var(--md-nav-on-section-color-convert);
|
|
||||||
background-color: var(--md-nav-section-color-convert);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:focus.convertto,
|
|
||||||
.dropdown-item:hover.convertto,
|
|
||||||
.dropdown-item.active.convertto {
|
|
||||||
color: var(--md-nav-on-section-color-convertto);
|
|
||||||
background-color: var(--md-nav-section-color-convertto);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:focus.image,
|
|
||||||
.dropdown-item:hover.image,
|
|
||||||
.dropdown-item.active.image {
|
|
||||||
color: var(--md-nav-on-section-color-image);
|
|
||||||
background-color: var(--md-nav-section-color-image);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:focus.word,
|
|
||||||
.dropdown-item:hover.word,
|
|
||||||
.dropdown-item.active.word {
|
|
||||||
color: var(--md-nav-on-section-color-word);
|
|
||||||
background-color: var(--md-nav-section-color-word);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:focus.ppt,
|
|
||||||
.dropdown-item:hover.ppt,
|
|
||||||
.dropdown-item.active.ppt {
|
|
||||||
color: var(--md-nav-on-section-color-ppt);
|
|
||||||
background-color: var(--md-nav-section-color-ppt);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:focus.security,
|
|
||||||
.dropdown-item:hover.security,
|
|
||||||
.dropdown-item.active.security {
|
|
||||||
color: var(--md-nav-on-section-color-security);
|
|
||||||
background-color: var(--md-nav-section-color-security);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:focus.other,
|
|
||||||
.dropdown-item:hover.other,
|
|
||||||
.dropdown-item.active.other {
|
|
||||||
color: var(--md-nav-on-section-color-other);
|
|
||||||
background-color: var(--md-nav-section-color-other);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:focus.advance,
|
|
||||||
.dropdown-item:hover.advance,
|
|
||||||
.dropdown-item.active.advance {
|
|
||||||
color: var(--md-nav-on-section-color-advance);
|
|
||||||
background-color: var(--md-nav-section-color-advance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dropdown min-width */
|
|
||||||
.dropdown-mw-28 {
|
|
||||||
min-width: 280px;
|
|
||||||
min-height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-mw-20 {
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dropdown open on hover */
|
|
||||||
html[dir="ltr"] .dropdown-menu {
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
top: auto;
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[dir="rtl"] .dropdown-menu {
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
top: auto;
|
|
||||||
left: 0;
|
|
||||||
right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bootstrap Popper positioning overrides removed - dropdowns now position naturally relative to their buttons */
|
|
||||||
|
|
||||||
.dropdown-menu-wrapper {
|
|
||||||
padding: 1.5rem 0;
|
|
||||||
border-radius: 1rem;
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
background-color: var(--md-sys-color-surface);
|
|
||||||
border: 1px solid var(--md-sys-color-surface-5);
|
|
||||||
box-shadow: var(--md-sys-elevation-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.dropdown-menu-tp {
|
|
||||||
color: transparent;
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-hide {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width:1199.98px) {
|
|
||||||
.navbar-collapse .dropdown-menu {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.navbar-collapse .dropdown-menu-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.navbar-collapse .dropdown-mw-28 {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width:1200px) {
|
|
||||||
/* This CSS-based hover is disabled because it conflicts with Bootstrap's JavaScript.
|
|
||||||
Hover functionality is now handled in navbar.js and search.js */
|
|
||||||
/*
|
|
||||||
.dropdown:hover .dropdown-menu {
|
|
||||||
display: block;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
.icon-hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.chevron-icon {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1199.98px) {
|
|
||||||
.navbar-collapse .dropdown-menu {
|
|
||||||
width: 100vw !important;
|
|
||||||
max-width: 100vw !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
transform-origin: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .navbar-expand-xl .dropdown-menu,
|
|
||||||
.navbar-expand .dropdown-menu {
|
|
||||||
transform: none !important;
|
|
||||||
transform-origin: none !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-collapse .dropdown-mega .dropdown-menu {
|
|
||||||
max-height: 60vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.navbar-collapse .dropdown-mega .dropdown-menu-wrapper {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#favoritesDropdown,
|
|
||||||
#languageDropdown + .dropdown-menu .dropdown-menu-wrapper,
|
|
||||||
#searchDropdown + .dropdown-menu .dropdown-menu-wrapper {
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#favoritesDropdown, #languageSelection, #searchResults {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-collapse .dropdown-menu-wrapper {
|
|
||||||
width: 95vw;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
.navbar-collapse .dropdown-mw-28 {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.icon-hide {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-collapse .dropdown-item {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
margin-left: auto !important;
|
|
||||||
margin-right: auto !important;
|
|
||||||
padding-left: 4px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mainNavbarDropdownMenu {
|
|
||||||
transform: none !important;
|
|
||||||
transform-origin: none !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu,
|
|
||||||
.dropdown-menu-tp,
|
|
||||||
.dropdown-menu-tp.show {
|
|
||||||
transform: none !important;
|
|
||||||
transform-origin: none !important;
|
|
||||||
max-width: 95vw !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.go-pro-link {
|
|
||||||
position: relative;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
z-index: 1;
|
|
||||||
display: inline-block;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.go-pro-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #007bff;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.go-pro-link:hover .go-pro-badge {
|
|
||||||
background-color: #0056b3;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#stacked {
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#stacked>.navbar-item {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.features-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 30px;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 14rem;
|
|
||||||
max-width: 18rem;
|
|
||||||
flex: 1 1 min(14rem, 100%);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.feature-group {
|
|
||||||
min-width: 10rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-rows {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0 1rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-rows.single-column {
|
|
||||||
justify-content: center;
|
|
||||||
/* Center-align a single column */
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-group-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
margin-top: 15px;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-group-container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.card-title.text-primary {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-card-icon {
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorite-icon {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
color: var(--md-sys-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorite-icon img {
|
|
||||||
filter: brightness(0) invert(var(--md-theme-filter-color));
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorite-icon:hover .material-symbols-rounded {
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorite-icon .material-symbols-rounded.fill {
|
|
||||||
color: #f5c000;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@keyframes Pulse {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-notice {
|
|
||||||
animation: scale 1s infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scale {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.96);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
.pdf-actions_button-container {
|
|
||||||
z-index: 4;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.1s linear;
|
|
||||||
|
|
||||||
position: absolute !important;
|
|
||||||
bottom: 0px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, 0%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_container:hover .pdf-actions_button-container {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_button-container>* {
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_button-container>*:focus {
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_button-container .btn {
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_button-container> :first-child,
|
|
||||||
.pdf-actions_container:first-child>.pdf-actions_button-container> :first-child+* {
|
|
||||||
border-radius: 12px 0px 0px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_container svg {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_container:nth-child(1) .pdf-actions_move-left-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_container:last-child .pdf-actions_move-right-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* "insert pdf" buttons that appear on the right when hover */
|
|
||||||
.pdf-actions_insert-file-button-container {
|
|
||||||
display:flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
translate: 0 -50%;
|
|
||||||
height: 100%;
|
|
||||||
width: 100px;
|
|
||||||
padding-left: 30px;
|
|
||||||
padding-right:30px;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 3;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.4s;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_insert-file-button-container.left {
|
|
||||||
left: -50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_insert-file-button-container.right {
|
|
||||||
right: -50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[dir="ltr"] .pdf-actions_insert-file-button-container.right {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[dir="rtl"] .pdf-actions_insert-file-button-container.left {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[dir="ltr"] .pdf-actions_container:last-child>.pdf-actions_insert-file-button-container.right {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[dir="rtl"] .pdf-actions_container:last-child>.pdf-actions_insert-file-button-container.left {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_insert-file-button-container.left button,
|
|
||||||
.pdf-actions_insert-file-button-container.right button {
|
|
||||||
padding: 0.45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_button-container button .material-symbols-rounded {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
vertical-align: sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_insert-file-button-container:hover {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.05s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions_checkbox {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
right: 3px;
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
background-color: var(--md-sys-color-surface-5);
|
|
||||||
padding: 6px 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
.filename {
|
|
||||||
flex-grow: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrows {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
.arrows .btn {
|
|
||||||
margin: 0 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.move-up span,
|
|
||||||
.move-down span {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
@@ -1,973 +0,0 @@
|
|||||||
/* Dark Mode Styles */
|
|
||||||
body,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
background-color: var(--md-sys-color-surface);
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.transition-theme {
|
|
||||||
transition: background 0.5s ease, color 0.5s ease, border 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*.global-buttons-container input:disabled::-webkit-input-placeholder { !* WebKit browsers *!*/
|
|
||||||
/* color: #98A0AB;*/
|
|
||||||
/*}*/
|
|
||||||
/*.global-buttons-container input:disabled:-moz-placeholder { !* Mozilla Firefox 4 to 18 *!*/
|
|
||||||
/* color: #98A0AB;*/
|
|
||||||
/*}*/
|
|
||||||
/*.global-buttons-container input:disabled::-moz-placeholder { !* Mozilla Firefox 19+ *!*/
|
|
||||||
/* color: #98A0AB;*/
|
|
||||||
/*}*/
|
|
||||||
/*.global-buttons-container input:disabled:-ms-input-placeholder { !* Internet Explorer 10+ *!*/
|
|
||||||
/* color: #98A0AB;*/
|
|
||||||
/*}*/
|
|
||||||
/* Scrollbar */
|
|
||||||
*::-webkit-scrollbar {
|
|
||||||
background: var(--md-sys-color-surface);
|
|
||||||
width: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-webkit-scrollbar-track {
|
|
||||||
background: var(--md-sys-color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 2rem;
|
|
||||||
background-color: var(--md-sys-color-surface-5);
|
|
||||||
border: 5px solid var(--md-sys-color-surface-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-webkit-scrollbar-corner {
|
|
||||||
background-color: var(--md-sys-color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Alerts */
|
|
||||||
.alert {
|
|
||||||
border-radius: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table */
|
|
||||||
td {
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group-append {
|
|
||||||
margin: 0rem 0.5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
background-color: transparent;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-card {
|
|
||||||
background-color: var(--md-sys-color-surface-5);
|
|
||||||
border-radius: 3rem;
|
|
||||||
padding: 2.5rem;
|
|
||||||
|
|
||||||
max-width: 95vw;
|
|
||||||
margin-left: 2vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 1.25rem;
|
|
||||||
border-radius: 2rem;
|
|
||||||
background-color: var(--md-sys-color-surface-5);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal */
|
|
||||||
.modal-content {
|
|
||||||
background-color: var(--md-sys-color-surface);
|
|
||||||
border-radius: 2rem;
|
|
||||||
border: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header,
|
|
||||||
.modal-body,
|
|
||||||
.modal-footer {
|
|
||||||
background-color: var(--md-sys-color-surface-5);
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
border-radius: 2rem 2rem 0rem 0rem;
|
|
||||||
padding: 1.5rem 2rem 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body{
|
|
||||||
padding: 0.5rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
border-radius: 0rem 0rem 2rem 2rem;
|
|
||||||
padding: 0.5rem 2rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Icon fill */
|
|
||||||
.material-symbols-rounded {
|
|
||||||
vertical-align: text-top;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navbar Icon*/
|
|
||||||
.nav-icon {
|
|
||||||
color: var(--md-sys-color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sign .nav-icon,
|
|
||||||
.sign.tool-header-icon {
|
|
||||||
color: var(--md-nav-on-section-color-sign);
|
|
||||||
background-color: var(--md-nav-section-color-sign);
|
|
||||||
}
|
|
||||||
|
|
||||||
.organize .nav-icon,
|
|
||||||
.organize.tool-header-icon {
|
|
||||||
color: var(--md-nav-on-section-color-organize);
|
|
||||||
background-color: var(--md-nav-section-color-organize);
|
|
||||||
}
|
|
||||||
|
|
||||||
.convert .nav-icon,
|
|
||||||
.convert.tool-header-icon {
|
|
||||||
color: var(--md-nav-on-section-color-convert);
|
|
||||||
background-color: var(--md-nav-section-color-convert);
|
|
||||||
}
|
|
||||||
|
|
||||||
.convertto .nav-icon,
|
|
||||||
.convertto.tool-header-icon {
|
|
||||||
color: var(--md-nav-on-section-color-convertto);
|
|
||||||
background-color: var(--md-nav-section-color-convertto);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.security .nav-icon,
|
|
||||||
.security.tool-header-icon {
|
|
||||||
color: var(--md-nav-on-section-color-security);
|
|
||||||
background-color: var(--md-nav-section-color-security);
|
|
||||||
}
|
|
||||||
|
|
||||||
.other .nav-icon,
|
|
||||||
.other.tool-header-icon {
|
|
||||||
color: var(--md-nav-on-section-color-other);
|
|
||||||
background-color: var(--md-nav-section-color-other);
|
|
||||||
}
|
|
||||||
|
|
||||||
.advance .nav-icon,
|
|
||||||
.advance.tool-header-icon {
|
|
||||||
color: var(--md-nav-on-section-color-advance);
|
|
||||||
background-color: var(--md-nav-section-color-advance);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image .nav-icon,
|
|
||||||
.image.tool-header-icon {
|
|
||||||
color: var(--md-nav-on-section-color-image);
|
|
||||||
background-color: var(--md-nav-section-color-image);
|
|
||||||
}
|
|
||||||
|
|
||||||
.word .nav-icon,
|
|
||||||
.word.tool-header-icon {
|
|
||||||
color: var(--md-nav-on-section-color-word);
|
|
||||||
background-color: var(--md-nav-section-color-word);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ppt .nav-icon,
|
|
||||||
.ppt.tool-header-icon {
|
|
||||||
color: var(--md-nav-on-section-color-ppt);
|
|
||||||
background-color: var(--md-nav-section-color-ppt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tool Page Header*/
|
|
||||||
.tool-header {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-header .tool-header-icon {
|
|
||||||
margin: 0px 1rem;
|
|
||||||
height: 4rem;
|
|
||||||
width: 4rem;
|
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 3rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-webkit-tap-highlight-color: rgb(0 0 0 / 0%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-header .tool-header-text {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 400;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Home Card Colors*/
|
|
||||||
.feature-card .nav-icon {
|
|
||||||
vertical-align: middle;
|
|
||||||
font-size: 2rem !important;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 0.9rem;
|
|
||||||
color: var(--md-sys-color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card .sign .nav-icon {
|
|
||||||
color: var(--md-nav-on-section-color-sign);
|
|
||||||
background-color: var(--md-nav-section-color-sign);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card .organize .nav-icon {
|
|
||||||
color: var(--md-nav-on-section-color-organize);
|
|
||||||
background-color: var(--md-nav-section-color-organize);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card .convert .nav-icon {
|
|
||||||
color: var(--md-nav-on-section-color-convert);
|
|
||||||
background-color: var(--md-nav-section-color-convert);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card .convertto .nav-icon {
|
|
||||||
color: var(--md-nav-on-section-color-convertto);
|
|
||||||
background-color: var(--md-nav-section-color-convertto);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card .security .nav-icon {
|
|
||||||
color: var(--md-nav-on-section-color-security);
|
|
||||||
background-color: var(--md-nav-section-color-security);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card .other .nav-icon {
|
|
||||||
color: var(--md-nav-on-section-color-other);
|
|
||||||
background-color: var(--md-nav-section-color-other);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card .advance .nav-icon {
|
|
||||||
color: var(--md-nav-on-section-color-advance);
|
|
||||||
background-color: var(--md-nav-section-color-advance);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card .image .nav-icon {
|
|
||||||
color: var(--md-nav-on-section-color-image);
|
|
||||||
background-color: var(--md-nav-section-color-image);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card .word .nav-icon {
|
|
||||||
color: var(--md-nav-on-section-color-word);
|
|
||||||
background-color: var(--md-nav-section-color-word);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card .ppt .nav-icon {
|
|
||||||
color: var(--md-nav-on-section-color-ppt);
|
|
||||||
background-color: var(--md-nav-section-color-ppt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons Components */
|
|
||||||
.btn {
|
|
||||||
border-radius: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-close {
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-close:hover {
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header .btn-close {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Primary btn */
|
|
||||||
.btn-primary {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary.disabled,
|
|
||||||
.btn-primary:disabled {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:active+.btn-primary,
|
|
||||||
.btn-check:checked+.btn-primary,
|
|
||||||
.btn-primary.active,
|
|
||||||
.btn-primary:active,
|
|
||||||
.show>.btn-primary.dropdown-toggle {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:focus+.btn-primary,
|
|
||||||
.btn-primary:focus {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Secondary btn */
|
|
||||||
.btn-secondary {
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
background-color: var(--md-sys-color-secondary);
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary.disabled,
|
|
||||||
.btn-secondary:disabled {
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
background-color: var(--md-sys-color-secondary);
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
background-color: var(--md-sys-color-secondary);
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:active+.btn-secondary,
|
|
||||||
.btn-check:checked+.btn-secondary,
|
|
||||||
.btn-secondary.active,
|
|
||||||
.btn-secondary:active,
|
|
||||||
.show>.btn-secondary.dropdown-toggle {
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
background-color: var(--md-sys-color-secondary);
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:focus+.btn-secondary,
|
|
||||||
.btn-secondary:focus {
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
background-color: var(--md-sys-color-secondary);
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Danger btn */
|
|
||||||
.btn-danger {
|
|
||||||
color: var(--md-sys-color-on-error);
|
|
||||||
background-color: var(--md-sys-color-error);
|
|
||||||
border-color: var(--md-sys-color-error);
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger.disabled,
|
|
||||||
.btn-danger:disabled {
|
|
||||||
color: var(--md-sys-color-on-error);
|
|
||||||
background-color: var(--md-sys-color-error);
|
|
||||||
border-color: var(--md-sys-color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
color: var(--md-sys-color-on-error);
|
|
||||||
background-color: var(--md-sys-color-error);
|
|
||||||
border-color: var(--md-sys-color-error);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:active+.btn-danger,
|
|
||||||
.btn-check:checked+.btn-danger,
|
|
||||||
.btn-danger.active,
|
|
||||||
.btn-danger:active,
|
|
||||||
.show>.btn-danger.dropdown-toggle {
|
|
||||||
color: var(--md-sys-color-on-error);
|
|
||||||
background-color: var(--md-sys-color-error);
|
|
||||||
border-color: var(--md-sys-color-error);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:focus+.btn-danger,
|
|
||||||
.btn-danger:focus {
|
|
||||||
color: var(--md-sys-color-on-error);
|
|
||||||
background-color: var(--md-sys-color-error);
|
|
||||||
border-color: var(--md-sys-color-error);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Info btn */
|
|
||||||
.btn-info {
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background-color: var(--md-sys-color-tertiary);
|
|
||||||
border-color: var(--md-sys-color-tertiary);
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-info .disabled,
|
|
||||||
.btn-info:disabled {
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background-color: var(--md-sys-color-tertiary);
|
|
||||||
border-color: var(--md-sys-color-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-info:hover {
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background-color: var(--md-sys-color-tertiary);
|
|
||||||
border-color: var(--md-sys-color-tertiary);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:active+.btn-info,
|
|
||||||
.btn-check:checked+.btn-info,
|
|
||||||
.btn-info .active,
|
|
||||||
.btn-info:active,
|
|
||||||
.show>.btn-info.dropdown-toggle {
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background-color: var(--md-sys-color-tertiary);
|
|
||||||
border-color: var(--md-sys-color-tertiary);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:focus+.btn-info,
|
|
||||||
.btn-info:focus {
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background-color: var(--md-sys-color-tertiary);
|
|
||||||
border-color: var(--md-sys-color-tertiary);
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Info btn */
|
|
||||||
.btn-success {
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success:hover,
|
|
||||||
.btn-check:active+.btn-success,
|
|
||||||
.btn-check:checked+.btn-success,
|
|
||||||
.btn-success .active,
|
|
||||||
.btn-success:active,
|
|
||||||
.show>.btn-success.dropdown-toggle,
|
|
||||||
.btn-check:focus+.btn-success,
|
|
||||||
.btn-success:focus {
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Warning btn */
|
|
||||||
.btn-warning {
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning:hover,
|
|
||||||
.btn-check:active+.btn-warning,
|
|
||||||
.btn-check:checked+.btn-warning,
|
|
||||||
.btn-warning .active,
|
|
||||||
.btn-warning:active,
|
|
||||||
.show>.btn-warning.dropdown-toggle,
|
|
||||||
.btn-check:focus+.btn-warning,
|
|
||||||
.btn-warning:focus {
|
|
||||||
box-shadow: var(--md-sys-elevation-3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Outline Primary btn */
|
|
||||||
.btn-outline-primary {
|
|
||||||
color: var(--md-sys-color-primary);
|
|
||||||
background-color: transparent;
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-primary .disabled,
|
|
||||||
.btn-outline-primary:disabled {
|
|
||||||
color: var(--md-sys-color-primary);
|
|
||||||
background-color: transparent;
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-primary:hover {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:active+.btn-outline-primary,
|
|
||||||
.btn-check:checked+.btn-outline-primary,
|
|
||||||
.btn-outline-primary .active,
|
|
||||||
.btn-outline-primary:active,
|
|
||||||
.show>.btn-outline-primary.dropdown-toggle {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:focus+.btn-outline-primary,
|
|
||||||
.btn-outline-primary:focus {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Outline Secondary btn */
|
|
||||||
.btn-outline-secondary {
|
|
||||||
color: var(--md-sys-color-secondary);
|
|
||||||
background-color: transparent;
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-secondary .disabled,
|
|
||||||
.btn-outline-secondary:disabled {
|
|
||||||
color: var(--md-sys-color-secondary);
|
|
||||||
background-color: transparent;
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-secondary:hover {
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
background-color: var(--md-sys-color-secondary);
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:active+.btn-outline-secondary,
|
|
||||||
.btn-check:checked+.btn-outline-secondary,
|
|
||||||
.btn-outline-secondary .active,
|
|
||||||
.btn-outline-secondary:active,
|
|
||||||
.show>.btn-outline-secondary.dropdown-toggle {
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
background-color: var(--md-sys-color-secondary);
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check:focus+.btn-outline-secondary,
|
|
||||||
.btn-outline-secondary:focus {
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
background-color: var(--md-sys-color-secondary);
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disabled btn */
|
|
||||||
.btn.disabled,
|
|
||||||
.btn:disabled,
|
|
||||||
fieldset:disabled .btn {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Range Slider */
|
|
||||||
.form-range{
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
.form-range:focus::-webkit-slider-thumb {
|
|
||||||
box-shadow: 0 0 0 1px var(--md-sys-color-surface), 0 0 0 .25rem var(--md-sys-color-primary)
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-range:focus::-moz-range-thumb {
|
|
||||||
box-shadow: 0 0 0 1px var(--md-sys-color-surface), 0 0 0 .25rem var(--md-sys-color-primary)
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-range::-webkit-slider-thumb {
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.form-range::-webkit-slider-thumb:active {
|
|
||||||
background-color: var(--md-sys-color-primary)
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-range::-webkit-slider-runnable-track {
|
|
||||||
background-color: var(--md-sys-color-on-primary)
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-range::-moz-range-thumb {
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* checkbox */
|
|
||||||
.form-check {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check-label {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check-input {
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
margin: 0;
|
|
||||||
background-color: var(--md-sys-color-surface);
|
|
||||||
border: 2px solid var(--md-sys-color-outline-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check-input:checked {
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
border-color: var(--md-sys-color-outline-variant);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check-input:focus {
|
|
||||||
border-color: var(--md-sys-color-outline-variant);
|
|
||||||
outline: 0;
|
|
||||||
box-shadow: 0 0 0 .25rem var(--md-sys-color-outline-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check-input:checked[type=checkbox] {
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check input[type="checkbox"]:checked+span.material-symbols-rounded {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check span.material-symbols-rounded {
|
|
||||||
display: none;
|
|
||||||
color: var(--md-sys-color-surface);
|
|
||||||
position: absolute;
|
|
||||||
margin-left: -1.5rem;
|
|
||||||
margin-right: -1.5rem;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-webkit-tap-highlight-color: rgb(0 0 0 / 0%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check {
|
|
||||||
min-height: 22px;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > label {
|
|
||||||
padding-left: 29px !important;
|
|
||||||
min-height: 22px;
|
|
||||||
line-height: 22px;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: top;
|
|
||||||
margin-bottom: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
cursor: pointer;
|
|
||||||
padding-right: 29px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input:first-child {
|
|
||||||
position: absolute !important;
|
|
||||||
opacity: 0;
|
|
||||||
margin: 0;
|
|
||||||
background-color: var(--md-sys-state-hover-opacity);
|
|
||||||
border-radius: 50%;
|
|
||||||
appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-ms-appearance: none;
|
|
||||||
display: block;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
outline: none;
|
|
||||||
transform: scale(1.65);
|
|
||||||
-ms-transform: scale(1.65);
|
|
||||||
transition: opacity .3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input:first-child:hover {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1.65);
|
|
||||||
-ms-transform: scale(1.65);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input:first-child:disabled {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input:first-child:disabled + label,
|
|
||||||
.form-check > input:first-child:disabled + input[type="hidden"] + label,
|
|
||||||
.form-check > input:first-child:disabled + label::before,
|
|
||||||
.form-check > input:first-child:disabled + input[type="hidden"] + label::before {
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: default;
|
|
||||||
filter: alpha(opacity=65);
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: .65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input:first-child + label::before,
|
|
||||||
.form-check > input:first-child + input[type="hidden"] + label::before {
|
|
||||||
content: "";
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border: 2px solid var(--md-sys-color-on-surface-variant);
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-left: -29px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-right: -29px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input:first-child:checked + label::after,
|
|
||||||
.form-check > input:first-child:checked + input[type="hidden"] + label::after {
|
|
||||||
content: "";
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 7px;
|
|
||||||
height: 10px;
|
|
||||||
border: solid 2px;
|
|
||||||
border-left: none;
|
|
||||||
border-top: none;
|
|
||||||
transform: translate(7.75px, 4.5px) rotate(45deg);
|
|
||||||
-ms-transform: translate(7.75px, 4.5px) rotate(45deg);
|
|
||||||
box-sizing: border-box;
|
|
||||||
right: 0;
|
|
||||||
margin-right: 14px;
|
|
||||||
border-bottom-color: var(--md-sys-color-on-primary);
|
|
||||||
border-right-color: var(--md-sys-color-on-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input:first-child::-ms-check {
|
|
||||||
opacity: 0;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input:first-child:active {
|
|
||||||
transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0s, transform 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input[type="radio"]:first-child + label::before,
|
|
||||||
.form-check > input[type="radio"]:first-child + input[type="hidden"] + label::before {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input[type="radio"]:first-child:checked + label::before,
|
|
||||||
.form-check > input[type="radio"]:first-child:checked + input[type="hidden"] + label::before {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input[type="radio"]:first-child:checked + label::after,
|
|
||||||
.form-check > input[type="radio"]:first-child:checked + input[type="hidden"] + label::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: none;
|
|
||||||
top: 6px;
|
|
||||||
left: 6px;
|
|
||||||
transform: none;
|
|
||||||
-ms-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input[type="checkbox"]:first-child:checked + label::after,
|
|
||||||
.form-check > input[type="checkbox"]:first-child:checked + input[type="hidden"] + label::after {
|
|
||||||
width: 8px;
|
|
||||||
height: 14px;
|
|
||||||
transform: translate(7px, 2px) rotate(45deg);
|
|
||||||
-ms-transform: translate(7px, 2px) rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check-inline {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check-inline + .form-check-inline {
|
|
||||||
margin-left: .75rem;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check > input:first-child:checked + label::before,
|
|
||||||
.form-check > input:first-child:checked + input[type="hidden"] + label::before {
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms */
|
|
||||||
textarea.form-control {
|
|
||||||
border-radius: 1.5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control,
|
|
||||||
.form-select,
|
|
||||||
.form-control:disabled,
|
|
||||||
.form-control[readonly] {
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
background-color: var(--md-sys-color-surface-container-low);
|
|
||||||
border-color: var(--md-sys-color-outline-variant);
|
|
||||||
border-radius: 3rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus,
|
|
||||||
.form-select:focus {
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
background-color: var(--md-sys-color-surface-container-lowest);
|
|
||||||
border-color: var(--md-sys-color-outline-variant);
|
|
||||||
outline: 0;
|
|
||||||
box-shadow: 0 0 0 0.25rem var(--md-sys-color-outline-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control-color {
|
|
||||||
padding: 0;
|
|
||||||
height: 2.4rem;
|
|
||||||
width: 2.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control input[type="color"] {
|
|
||||||
opacity: 0;
|
|
||||||
height: 2.4rem;
|
|
||||||
width: 2.4rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control input[type="color"]:hover{
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navbar Components */
|
|
||||||
.navbar-brand {
|
|
||||||
color: var(--md-sys-color-on-surface) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: none !important;
|
|
||||||
padding: 0.5rem 1rem !important;
|
|
||||||
border: 1px transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-nav li {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-nav .nav-link {
|
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-nav .nav-link:focus,
|
|
||||||
.navbar-nav .nav-link:hover {
|
|
||||||
color: var(--md-sys-color-on-secondary-container);
|
|
||||||
background-color: var(--md-sys-color-surface-3);
|
|
||||||
border-radius: 3rem;
|
|
||||||
font-weight: 500;
|
|
||||||
font-variation-settings: var(--md-sys-icon-fill-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-nav .nav-link.active,
|
|
||||||
.navbar-nav .show>.nav-link {
|
|
||||||
color: var(--md-sys-color-on-secondary-container);
|
|
||||||
background-color: var(--md-sys-color-surface-5);
|
|
||||||
border-radius: 3rem;
|
|
||||||
font-weight: 500;
|
|
||||||
font-variation-settings: var(--md-sys-icon-fill-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-title {
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
margin: 0 1%;
|
|
||||||
padding: 1.5rem 0;
|
|
||||||
border-radius: 1rem;
|
|
||||||
min-width: 20rem;
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
background-color: var(--md-sys-color-surface-container);
|
|
||||||
border: 1px solid var(--md-sys-color-surface-5);
|
|
||||||
box-shadow: var(--md-sys-elevation-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item {
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
padding: 0.25rem 1rem;
|
|
||||||
border-radius: 3rem;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:focus,
|
|
||||||
.dropdown-item:hover {
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
background-color: var(--md-sys-color-surface-5);
|
|
||||||
border-radius: 3rem;
|
|
||||||
font-variation-settings: var(--md-sys-icon-fill-1);
|
|
||||||
}
|
|
||||||
.dropdown-item.no-hover:hover,
|
|
||||||
.dropdown-item.no-hover:focus {
|
|
||||||
color: var(--md-sys-color-on-surface) !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
border-radius: 3rem !important;
|
|
||||||
font-variation-settings: initial !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item.active,
|
|
||||||
.dropdown-item:active {
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
background-color: var(--md-sys-color-surface-5);
|
|
||||||
border-radius: 3rem;
|
|
||||||
font-weight: 500;
|
|
||||||
font-variation-settings: var(--md-sys-icon-fill-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* list-group-item */
|
|
||||||
.list-group-item {
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
background-color: var(--md-sys-color-surface-5);
|
|
||||||
border: 1px solid var(--md-sys-color-outline-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item:first-child {
|
|
||||||
border-radius: 1rem 1rem 0rem 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item:last-child {
|
|
||||||
border-radius: 0rem 0rem 1rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item:only-child {
|
|
||||||
border-radius: 1rem 1rem 1rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item .btn {
|
|
||||||
padding: .375rem .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*Alert */
|
|
||||||
.alert-container {
|
|
||||||
padding: 2rem 3rem;
|
|
||||||
border-radius: 3rem;
|
|
||||||
margin: 1rem 0rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-header {
|
|
||||||
display: flex !important;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-heading {
|
|
||||||
font-size: calc(1.275rem + .3vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-dismissible .btn-close {
|
|
||||||
position: relative;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-danger {
|
|
||||||
color: var(--md-sys-color-on-error-container);
|
|
||||||
background-color: var(--md-sys-color-error-container);
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Material Symbols Rounded';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100 700;
|
|
||||||
src: url(../../fonts/google-symbol.woff2) format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.material-symbols-rounded {
|
|
||||||
font-family: 'Material Symbols Rounded';
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: normal;
|
|
||||||
text-transform: none;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
word-wrap: normal;
|
|
||||||
direction: ltr;
|
|
||||||
-webkit-font-feature-settings: 'liga';
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
:where(html, .light-theme, .dark-theme),
|
|
||||||
.tokens,
|
|
||||||
:host {
|
|
||||||
/* Define surface colors based on primary color */
|
|
||||||
--md-sys-color-surface-1: color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 100, 0.05) 5%);
|
|
||||||
--md-sys-color-surface-2: color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 255, 0.08) 5%);
|
|
||||||
--md-sys-color-surface-3: color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 255, 0.11) 5%);
|
|
||||||
--md-sys-color-surface-4: color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 255, 0.12) 5%);
|
|
||||||
--md-sys-color-surface-5: color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 255, 0.14) 5%);
|
|
||||||
/* Clear button disabled text color (default/light) */
|
|
||||||
--spdf-clear-disabled-text: var(--md-sys-color-primary);
|
|
||||||
/* Icon fill */
|
|
||||||
--md-sys-icon-fill-0: 'FILL' 0, 'wght' 500;
|
|
||||||
--md-sys-icon-fill-1: 'FILL' 1, 'wght' 500;
|
|
||||||
/* Hover Color */
|
|
||||||
--md-sys-state-hover-opacity: color-mix(in srgb, var(--md-sys-color-primary), rgba(0, 0, 0, 0) 80%);
|
|
||||||
/* Shadow */
|
|
||||||
--md-sys-color-shadow: #000000;
|
|
||||||
--md-elevation-shadow-color-rgb: 0, 0, 0;
|
|
||||||
--md-elevation-shadow-color: var(--md-elevation-shadow-color-rgb);
|
|
||||||
/* Shadow Elevation*/
|
|
||||||
--md-sys-elevation-0: 0px 0px 0px 0px rgb(var(--md-elevation-shadow-color), 0.2), 0px 0px 0px 0px rgb(var(--md-elevation-shadow-color), 0.14), 0px 0px 0px 0px rgb(var(--md-elevation-shadow-color), 0.12);
|
|
||||||
--md-sys-elevation-1: 0px 3px 1px -2px rgb(var(--md-elevation-shadow-color), 0.2), 0px 2px 2px 0px rgb(var(--md-elevation-shadow-color), 0.14), 0px 1px 5px 0px rgb(var(--md-elevation-shadow-color), 0.12);
|
|
||||||
--md-sys-elevation-2: 0px 2px 4px -1px rgb(var(--md-elevation-shadow-color), 0.2), 0px 4px 5px 0px rgb(var(--md-elevation-shadow-color), 0.14), 0px 1px 10px 0px rgb(var(--md-elevation-shadow-color), 0.12);
|
|
||||||
--md-sys-elevation-3: 0px 5px 5px -3px rgb(var(--md-elevation-shadow-color), 0.2), 0px 8px 10px 1px rgb(var(--md-elevation-shadow-color), 0.14), 0px 3px 14px 2px rgb(var(--md-elevation-shadow-color), 0.12);
|
|
||||||
--md-sys-elevation-4: 0px 5px 5px -3px rgb(var(--md-elevation-shadow-color) / 0.2), 0px 8px 10px 1px rgb(var(--md-elevation-shadow-color), 0.14), 0px 3px 14px 2px rgb(var(--md-elevation-shadow-color), 0.12);
|
|
||||||
--md-sys-elevation-5: 0px 8px 10px -6px rgb(var(--md-elevation-shadow-color), 0.2), 0px 16px 24px 2px rgb(var(--md-elevation-shadow-color), 0.14), 0px 6px 30px 5px rgb(var(--md-elevation-shadow-color), 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark theme overrides */
|
|
||||||
.dark-theme {
|
|
||||||
/* In dark mode, use a neutral grey for disabled Clear button text */
|
|
||||||
--spdf-clear-disabled-text: var(--mantine-color-gray-5, #9e9e9e);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fill {
|
|
||||||
font-variation-settings: var(--md-sys-icon-fill-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-fill {
|
|
||||||
/* font-variation-settings: var(--md-sys-icon-fill-0); */
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
:root {
|
|
||||||
/* Colors */
|
|
||||||
--md-sys-color-primary: rgb(162 201 255);
|
|
||||||
--md-sys-color-surface-tint: rgb(162 201 255);
|
|
||||||
--md-sys-color-on-primary: rgb(0 49 92);
|
|
||||||
--md-sys-color-primary-container: rgb(0 118 208);
|
|
||||||
--md-sys-color-on-primary-container: rgb(255 255 255);
|
|
||||||
--md-sys-color-secondary: rgb(169 201 246);
|
|
||||||
--md-sys-color-on-secondary: rgb(12 49 87);
|
|
||||||
--md-sys-color-secondary-container: rgb(29 62 100);
|
|
||||||
--md-sys-color-on-secondary-container: rgb(180 210 255);
|
|
||||||
--md-sys-color-tertiary: rgb(193 194 248);
|
|
||||||
--md-sys-color-on-tertiary: rgb(42 44 88);
|
|
||||||
--md-sys-color-tertiary-container: rgb(110 112 161);
|
|
||||||
--md-sys-color-on-tertiary-container: rgb(255 255 255);
|
|
||||||
--md-sys-color-error: rgb(255 180 171);
|
|
||||||
--md-sys-color-on-error: rgb(105 0 5);
|
|
||||||
--md-sys-color-error-container: rgb(147 0 10);
|
|
||||||
--md-sys-color-on-error-container: rgb(255 218 214);
|
|
||||||
--md-sys-color-background: rgb(15 20 26);
|
|
||||||
--md-sys-color-on-background: rgb(223 226 235);
|
|
||||||
--md-sys-color-surface: rgb(15 20 26);
|
|
||||||
--md-sys-color-on-surface: rgb(223 226 235);
|
|
||||||
--md-sys-color-surface-variant: rgb(64 71 83);
|
|
||||||
--md-sys-color-on-surface-variant: rgb(192 199 213);
|
|
||||||
--md-sys-color-outline: rgb(138 145 158);
|
|
||||||
--md-sys-color-outline-variant: rgb(64 71 83);
|
|
||||||
--md-sys-color-shadow: rgb(0 0 0);
|
|
||||||
--md-sys-color-scrim: rgb(0 0 0);
|
|
||||||
--md-sys-color-inverse-surface: rgb(223 226 235);
|
|
||||||
--md-sys-color-inverse-on-surface: rgb(45 49 55);
|
|
||||||
--md-sys-color-inverse-primary: rgb(0 96 170);
|
|
||||||
--md-sys-color-primary-fixed: rgb(211 228 255);
|
|
||||||
--md-sys-color-on-primary-fixed: rgb(0 28 56);
|
|
||||||
--md-sys-color-primary-fixed-dim: rgb(162 201 255);
|
|
||||||
--md-sys-color-on-primary-fixed-variant: rgb(0 72 130);
|
|
||||||
--md-sys-color-secondary-fixed: rgb(211 228 255);
|
|
||||||
--md-sys-color-on-secondary-fixed: rgb(0 28 56);
|
|
||||||
--md-sys-color-secondary-fixed-dim: rgb(169 201 246);
|
|
||||||
--md-sys-color-on-secondary-fixed-variant: rgb(40 72 111);
|
|
||||||
--md-sys-color-tertiary-fixed: rgb(225 224 255);
|
|
||||||
--md-sys-color-on-tertiary-fixed: rgb(20 22 66);
|
|
||||||
--md-sys-color-tertiary-fixed-dim: rgb(193 194 248);
|
|
||||||
--md-sys-color-on-tertiary-fixed-variant: rgb(64 67 112);
|
|
||||||
--md-sys-color-surface-dim: rgb(15 20 26);
|
|
||||||
--md-sys-color-surface-bright: rgb(53 57 64);
|
|
||||||
--md-sys-color-surface-container-lowest: rgb(10 14 20);
|
|
||||||
--md-sys-color-surface-container-low: rgb(24 28 34);
|
|
||||||
--md-sys-color-surface-container: rgb(28 32 38);
|
|
||||||
--md-sys-color-surface-container-high: rgb(38 42 49);
|
|
||||||
--md-sys-color-surface-container-highest: rgb(49 53 60);
|
|
||||||
/* Tools Color */
|
|
||||||
--md-nav-section-color-opacity: 1;
|
|
||||||
--md-nav-on-section-color-opacity: 1;
|
|
||||||
--md-nav-section-color-sign: rgba(25, 101, 212, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-sign: rgba(28, 27, 31, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-organize: rgba(120, 130, 255, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-organize: rgba(28, 27, 31, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-convert: rgba(25, 177, 212, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-convert: rgba(28, 27, 31, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-convertto: rgba(104, 220, 149, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-convertto: rgba(28, 27, 31, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-security: rgba(255, 120, 146, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-security: rgba(28, 27, 31, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-other: rgba(72, 189, 84, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-other: rgba(28, 27, 31, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-advance: rgba(245, 84, 84, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-advance: rgba(28, 27, 31, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-image: rgba(212, 172, 25, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-image: rgba(28, 27, 31, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-word: rgba(61, 153, 245, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-word: rgba(28, 27, 31, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-ppt: rgba(255, 128, 0, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-ppt: rgba(28, 27, 31, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-color-on-separator: rgb(24 28 34);
|
|
||||||
--md-nav-background: rgb(15 20 26);
|
|
||||||
--favourite-add: #9ed18c;
|
|
||||||
--favourite-remove: palevioletred;
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
:root {
|
|
||||||
/* Colors */
|
|
||||||
--md-sys-color-primary: rgb(0 96 170);
|
|
||||||
--md-sys-color-surface-tint: rgb(0 96 170);
|
|
||||||
--md-sys-color-on-primary: rgb(255 255 255);
|
|
||||||
--md-sys-color-primary-container: rgb(80 163 255);
|
|
||||||
--md-sys-color-on-primary-container: rgb(0 20 43);
|
|
||||||
--md-sys-color-secondary: rgb(65 96 136);
|
|
||||||
--md-sys-color-on-secondary: rgb(255 255 255);
|
|
||||||
--md-sys-color-secondary-container: rgb(188 215 255);
|
|
||||||
--md-sys-color-on-secondary-container: rgb(32 65 103);
|
|
||||||
--md-sys-color-tertiary: rgb(88 90 138);
|
|
||||||
--md-sys-color-on-tertiary: rgb(255 255 255);
|
|
||||||
--md-sys-color-tertiary-container: rgb(151 153 205);
|
|
||||||
--md-sys-color-on-tertiary-container: rgb(7 9 55);
|
|
||||||
--md-sys-color-error: rgb(186 26 26);
|
|
||||||
--md-sys-color-on-error: rgb(255 255 255);
|
|
||||||
--md-sys-color-error-container: rgb(255 218 214);
|
|
||||||
--md-sys-color-on-error-container: rgb(65 0 2);
|
|
||||||
--md-sys-color-background: rgb(248 249 255);
|
|
||||||
--md-sys-color-on-background: rgb(24 28 34);
|
|
||||||
--md-sys-color-surface: rgb(237, 240, 245);
|
|
||||||
--md-sys-color-on-surface: rgb(0, 1, 1);
|
|
||||||
--md-sys-color-surface-variant: rgb(220 227 241);
|
|
||||||
--md-sys-color-on-surface-variant: rgb(64 71 83);
|
|
||||||
--md-sys-color-outline: rgb(112 119 132);
|
|
||||||
--md-sys-color-outline-variant: rgb(192 199 213);
|
|
||||||
--md-sys-color-shadow: rgb(0 0 0);
|
|
||||||
--md-sys-color-scrim: rgb(0 0 0);
|
|
||||||
--md-sys-color-inverse-surface: rgb(45 49 55);
|
|
||||||
--md-sys-color-inverse-on-surface: rgb(238 241 250);
|
|
||||||
--md-sys-color-inverse-primary: rgb(162 201 255);
|
|
||||||
--md-sys-color-primary-fixed: rgb(211 228 255);
|
|
||||||
--md-sys-color-on-primary-fixed: rgb(0 28 56);
|
|
||||||
--md-sys-color-primary-fixed-dim: rgb(162 201 255);
|
|
||||||
--md-sys-color-on-primary-fixed-variant: rgb(0 72 130);
|
|
||||||
--md-sys-color-secondary-fixed: rgb(211 228 255);
|
|
||||||
--md-sys-color-on-secondary-fixed: rgb(0 28 56);
|
|
||||||
--md-sys-color-secondary-fixed-dim: rgb(169 201 246);
|
|
||||||
--md-sys-color-on-secondary-fixed-variant: rgb(40 72 111);
|
|
||||||
--md-sys-color-tertiary-fixed: rgb(225 224 255);
|
|
||||||
--md-sys-color-on-tertiary-fixed: rgb(20 22 66);
|
|
||||||
--md-sys-color-tertiary-fixed-dim: rgb(193 194 248);
|
|
||||||
--md-sys-color-on-tertiary-fixed-variant: rgb(64 67 112);
|
|
||||||
--md-sys-color-surface-dim: rgb(215 218 227);
|
|
||||||
--md-sys-color-surface-bright: rgb(248 249 255);
|
|
||||||
--md-sys-color-surface-container-lowest: rgb(255 255 255);
|
|
||||||
--md-sys-color-surface-container-low: rgb(241 243 253);
|
|
||||||
--md-sys-color-surface-container: rgb(235 238 247);
|
|
||||||
--md-sys-color-surface-container-high: rgb(229 232 241);
|
|
||||||
--md-sys-color-surface-container-highest: rgb(223 226 235);
|
|
||||||
/* Tools Color */
|
|
||||||
--md-nav-section-color-opacity: 1;
|
|
||||||
--md-nav-on-section-color-opacity: 1;
|
|
||||||
--md-nav-section-color-sign: rgba(25, 101, 212, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-sign: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-organize: rgba(120, 130, 255, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-organize: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-convert: rgba(25, 177, 212, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-convert: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-convertto: rgba(104, 220, 149, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-convertto: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-security: rgba(255, 120, 146, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-security: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-other: rgba(72, 189, 84, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-other: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-advance: rgba(245, 84, 84, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-advance: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-image: rgba(212, 172, 25, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-image: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-word: rgba(61, 153, 245, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-word: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-section-color-ppt: rgba(255, 128, 0, var(--md-nav-section-color-opacity));
|
|
||||||
--md-nav-on-section-color-ppt: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
|
||||||
--md-nav-color-on-separator: rgb(174, 178, 179);
|
|
||||||
--md-nav-background: rgb(248 249 255);
|
|
||||||
--favourite-add: #25ab6c;
|
|
||||||
--favourite-remove: rgb(222, 94, 137);
|
|
||||||
}
|
|
||||||
-96
@@ -1,96 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
|
||||||
import org.junit.jupiter.params.provider.CsvSource;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
|
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
|
||||||
|
|
||||||
@ExtendWith({MockitoExtension.class})
|
|
||||||
class RearrangePagesPDFControllerTest {
|
|
||||||
|
|
||||||
@Mock private CustomPDFDocumentFactory mockPdfDocumentFactory;
|
|
||||||
|
|
||||||
private RearrangePagesPDFController sut;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
sut = new RearrangePagesPDFController(mockPdfDocumentFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Tests the behavior of the oddEvenMerge method when there are no pages in the document. */
|
|
||||||
@Test
|
|
||||||
void oddEvenMerge_noPages() {
|
|
||||||
int totalNumberOfPages = 0;
|
|
||||||
|
|
||||||
List<Integer> newPageOrder = sut.oddEvenMerge(totalNumberOfPages);
|
|
||||||
|
|
||||||
assertNotNull(newPageOrder, "Returning null instead of page order list");
|
|
||||||
assertEquals(List.of(), newPageOrder, "Page order doesn't match");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests the behavior of the oddEvenMerge method when there are odd total pages in the document.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void oddEvenMerge_oddTotalPageNumber() {
|
|
||||||
int totalNumberOfPages = 5;
|
|
||||||
|
|
||||||
List<Integer> newPageOrder = sut.oddEvenMerge(totalNumberOfPages);
|
|
||||||
|
|
||||||
assertNotNull(newPageOrder, "Returning null instead of page order list");
|
|
||||||
assertEquals(Arrays.asList(0, 3, 1, 4, 2), newPageOrder, "Page order doesn't match");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests the behavior of the oddEvenMerge method when there are even total pages in the
|
|
||||||
* document.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void oddEvenMerge_evenTotalPageNumber() {
|
|
||||||
int totalNumberOfPages = 6;
|
|
||||||
|
|
||||||
List<Integer> newPageOrder = sut.oddEvenMerge(totalNumberOfPages);
|
|
||||||
|
|
||||||
assertNotNull(newPageOrder, "Returning null instead of page order list");
|
|
||||||
assertEquals(Arrays.asList(0, 3, 1, 4, 2, 5), newPageOrder, "Page order doesn't match");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests the behavior of the oddEvenMerge method with multiple test cases of multiple pages.
|
|
||||||
*
|
|
||||||
* @param totalNumberOfPages The total number of pages in the document.
|
|
||||||
* @param expectedPageOrder The expected order of the pages after rearranging.
|
|
||||||
*/
|
|
||||||
@ParameterizedTest
|
|
||||||
@CsvSource({
|
|
||||||
"1, '0'",
|
|
||||||
"2, '0,1'",
|
|
||||||
"3, '0,2,1'",
|
|
||||||
"4, '0,2,1,3'",
|
|
||||||
"5, '0,3,1,4,2'",
|
|
||||||
"6, '0,3,1,4,2,5'",
|
|
||||||
"10, '0,5,1,6,2,7,3,8,4,9'",
|
|
||||||
"50, '0,25,1,26,2,27,3,28,4,29,5,30,6,31,7,32,8,33,9,34,10,35,"
|
|
||||||
+ "11,36,12,37,13,38,14,39,15,40,16,41,17,42,18,43,19,44,20,45,21,46,"
|
|
||||||
+ "22,47,23,48,24,49'"
|
|
||||||
})
|
|
||||||
void oddEvenMerge_multi_test(int totalNumberOfPages, String expectedPageOrder) {
|
|
||||||
List<Integer> newPageOrder = sut.oddEvenMerge(totalNumberOfPages);
|
|
||||||
|
|
||||||
assertNotNull(newPageOrder, "Returning null instead of page order list");
|
|
||||||
assertEquals(
|
|
||||||
Arrays.stream(expectedPageOrder.split(",")).map(Integer::parseInt).toList(),
|
|
||||||
newPageOrder,
|
|
||||||
"Page order doesn't match");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,6 @@ class SortTypesTest {
|
|||||||
"BOOKLET_SORT",
|
"BOOKLET_SORT",
|
||||||
"SIDE_STITCH_BOOKLET_SORT",
|
"SIDE_STITCH_BOOKLET_SORT",
|
||||||
"ODD_EVEN_SPLIT",
|
"ODD_EVEN_SPLIT",
|
||||||
"ODD_EVEN_MERGE",
|
|
||||||
"REMOVE_FIRST",
|
"REMOVE_FIRST",
|
||||||
"REMOVE_LAST",
|
"REMOVE_LAST",
|
||||||
"REMOVE_FIRST_AND_LAST",
|
"REMOVE_FIRST_AND_LAST",
|
||||||
|
|||||||
-146
@@ -1,146 +0,0 @@
|
|||||||
package stirling.software.SPDF.service;
|
|
||||||
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.cos.COSName;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDResources;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
|
|
||||||
class PdfImageRemovalServiceTest {
|
|
||||||
|
|
||||||
private PdfImageRemovalService service;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
service = new PdfImageRemovalService();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper method for matching COSName in verification
|
|
||||||
private static COSName eq(final COSName value) {
|
|
||||||
return Mockito.argThat(
|
|
||||||
new org.mockito.ArgumentMatcher<>() {
|
|
||||||
@Override
|
|
||||||
public boolean matches(COSName argument) {
|
|
||||||
if (argument == null && value == null) return true;
|
|
||||||
if (argument == null || value == null) return false;
|
|
||||||
return argument.getName().equals(value.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "eq(" + (value != null ? value.getName() : "null") + ")";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testRemoveImagesFromPdf_WithImages() throws IOException {
|
|
||||||
// Mock PDF document and its components
|
|
||||||
PDDocument document = mock(PDDocument.class);
|
|
||||||
PDPage page = mock(PDPage.class);
|
|
||||||
PDResources resources = mock(PDResources.class);
|
|
||||||
PDPageTree pageTree = mock(PDPageTree.class);
|
|
||||||
|
|
||||||
// Configure page tree to iterate over our single page
|
|
||||||
when(document.getPages()).thenReturn(pageTree);
|
|
||||||
Iterator<PDPage> pageIterator = Collections.singletonList(page).iterator();
|
|
||||||
when(pageTree.iterator()).thenReturn(pageIterator);
|
|
||||||
|
|
||||||
// Set up page resources
|
|
||||||
when(page.getResources()).thenReturn(resources);
|
|
||||||
|
|
||||||
// Set up image XObjects
|
|
||||||
COSName img1 = COSName.getPDFName("Im1");
|
|
||||||
COSName img2 = COSName.getPDFName("Im2");
|
|
||||||
COSName nonImg = COSName.getPDFName("NonImg");
|
|
||||||
|
|
||||||
List<COSName> xObjectNames = Arrays.asList(img1, img2, nonImg);
|
|
||||||
when(resources.getXObjectNames()).thenReturn(xObjectNames);
|
|
||||||
|
|
||||||
// Configure which are image XObjects
|
|
||||||
when(resources.isImageXObject(img1)).thenReturn(true);
|
|
||||||
when(resources.isImageXObject(img2)).thenReturn(true);
|
|
||||||
when(resources.isImageXObject(nonImg)).thenReturn(false);
|
|
||||||
|
|
||||||
// Execute the method
|
|
||||||
service.removeImagesFromPdf(document);
|
|
||||||
|
|
||||||
// Verify that images were removed
|
|
||||||
verify(resources, times(1)).put(eq(img1), Mockito.<PDXObject>isNull());
|
|
||||||
verify(resources, times(1)).put(eq(img2), Mockito.<PDXObject>isNull());
|
|
||||||
verify(resources, never()).put(eq(nonImg), Mockito.<PDXObject>isNull());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testRemoveImagesFromPdf_NoImages() throws IOException {
|
|
||||||
// Mock PDF document and its components
|
|
||||||
PDDocument document = mock(PDDocument.class);
|
|
||||||
PDPage page = mock(PDPage.class);
|
|
||||||
PDResources resources = mock(PDResources.class);
|
|
||||||
PDPageTree pageTree = mock(PDPageTree.class);
|
|
||||||
|
|
||||||
// Configure page tree to iterate over our single page
|
|
||||||
when(document.getPages()).thenReturn(pageTree);
|
|
||||||
Iterator<PDPage> pageIterator = Collections.singletonList(page).iterator();
|
|
||||||
when(pageTree.iterator()).thenReturn(pageIterator);
|
|
||||||
|
|
||||||
// Set up page resources
|
|
||||||
when(page.getResources()).thenReturn(resources);
|
|
||||||
|
|
||||||
// Create empty list of XObject names
|
|
||||||
List<COSName> emptyList = new ArrayList<>();
|
|
||||||
when(resources.getXObjectNames()).thenReturn(emptyList);
|
|
||||||
|
|
||||||
// Execute the method
|
|
||||||
service.removeImagesFromPdf(document);
|
|
||||||
|
|
||||||
// Verify that no modifications were made
|
|
||||||
verify(resources, never()).put(any(COSName.class), any(PDXObject.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testRemoveImagesFromPdf_MultiplePages() throws IOException {
|
|
||||||
// Mock PDF document and its components
|
|
||||||
PDDocument document = mock(PDDocument.class);
|
|
||||||
PDPage page1 = mock(PDPage.class);
|
|
||||||
PDPage page2 = mock(PDPage.class);
|
|
||||||
PDResources resources1 = mock(PDResources.class);
|
|
||||||
PDResources resources2 = mock(PDResources.class);
|
|
||||||
PDPageTree pageTree = mock(PDPageTree.class);
|
|
||||||
|
|
||||||
// Configure page tree to iterate over our two pages
|
|
||||||
when(document.getPages()).thenReturn(pageTree);
|
|
||||||
Iterator<PDPage> pageIterator = Arrays.asList(page1, page2).iterator();
|
|
||||||
when(pageTree.iterator()).thenReturn(pageIterator);
|
|
||||||
|
|
||||||
// Set up page resources
|
|
||||||
when(page1.getResources()).thenReturn(resources1);
|
|
||||||
when(page2.getResources()).thenReturn(resources2);
|
|
||||||
|
|
||||||
// Set up image XObjects for page 1
|
|
||||||
COSName img1 = COSName.getPDFName("Im1");
|
|
||||||
when(resources1.getXObjectNames()).thenReturn(Collections.singletonList(img1));
|
|
||||||
when(resources1.isImageXObject(img1)).thenReturn(true);
|
|
||||||
|
|
||||||
// Set up image XObjects for page 2
|
|
||||||
COSName img2 = COSName.getPDFName("Im2");
|
|
||||||
when(resources2.getXObjectNames()).thenReturn(Collections.singletonList(img2));
|
|
||||||
when(resources2.isImageXObject(img2)).thenReturn(true);
|
|
||||||
|
|
||||||
// Execute the method
|
|
||||||
service.removeImagesFromPdf(document);
|
|
||||||
|
|
||||||
// Verify that images were removed from both pages
|
|
||||||
verify(resources1, times(1)).put(eq(img1), Mockito.<PDXObject>isNull());
|
|
||||||
verify(resources2, times(1)).put(eq(img2), Mockito.<PDXObject>isNull());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user