mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 16:05:09 +00:00
wip - adding missing files
This commit is contained in:
parent
798f4570d5
commit
ea04596e35
@ -1,6 +1,5 @@
|
|||||||
package stirling.software.common.configuration;
|
package stirling.software.common.configuration;
|
||||||
|
|
||||||
import com.posthog.java.PostHog;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@ -48,12 +47,6 @@ public class AppConfig {
|
|||||||
@Value("${server.port:8080}")
|
@Value("${server.port:8080}")
|
||||||
private String serverPort;
|
private String serverPort;
|
||||||
|
|
||||||
@Value("${posthog.apiKey")
|
|
||||||
private String posthogApiKey;
|
|
||||||
|
|
||||||
@Value("${posthog.host}")
|
|
||||||
private String posthogHost;
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||||
@ -277,12 +270,4 @@ public class AppConfig {
|
|||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public PostHog postHog() {
|
|
||||||
return new PostHog.Builder(posthogApiKey)
|
|
||||||
.host(posthogHost)
|
|
||||||
.logger(new PostHogLoggerImpl())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
package stirling.software.common.configuration;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import com.posthog.java.PostHog;
|
||||||
|
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Slf4j
|
||||||
|
public class PostHogConfig {
|
||||||
|
|
||||||
|
@Value("${posthog.api.key}")
|
||||||
|
private String posthogApiKey;
|
||||||
|
|
||||||
|
@Value("${posthog.host}")
|
||||||
|
private String posthogHost;
|
||||||
|
|
||||||
|
private PostHog postHogClient;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PostHog postHogClient() {
|
||||||
|
postHogClient =
|
||||||
|
new PostHog.Builder(posthogApiKey)
|
||||||
|
.host(posthogHost)
|
||||||
|
.logger(new PostHogLoggerImpl())
|
||||||
|
.build();
|
||||||
|
return postHogClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdownPostHog() {
|
||||||
|
if (postHogClient != null) {
|
||||||
|
postHogClient.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,317 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.security.authentication.ProviderManager;
|
||||||
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||||
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||||
|
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
|
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||||
|
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||||
|
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||||
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||||
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||||
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler;
|
||||||
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
|
||||||
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
|
||||||
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
import stirling.software.SPDF.repository.PersistentLoginRepository;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity
|
||||||
|
@Slf4j
|
||||||
|
@DependsOn("runningProOrHigher")
|
||||||
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
|
private final CustomUserDetailsService userDetailsService;
|
||||||
|
private final UserService userService;
|
||||||
|
private final boolean loginEnabledValue;
|
||||||
|
private final boolean runningProOrHigher;
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
private final UserAuthenticationFilter userAuthenticationFilter;
|
||||||
|
private final LoginAttemptService loginAttemptService;
|
||||||
|
private final FirstLoginFilter firstLoginFilter;
|
||||||
|
private final SessionPersistentRegistry sessionRegistry;
|
||||||
|
private final PersistentLoginRepository persistentLoginRepository;
|
||||||
|
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
||||||
|
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
||||||
|
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||||
|
|
||||||
|
public SecurityConfiguration(
|
||||||
|
PersistentLoginRepository persistentLoginRepository,
|
||||||
|
CustomUserDetailsService userDetailsService,
|
||||||
|
@Lazy UserService userService,
|
||||||
|
@Qualifier("loginEnabled") boolean loginEnabledValue,
|
||||||
|
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
UserAuthenticationFilter userAuthenticationFilter,
|
||||||
|
LoginAttemptService loginAttemptService,
|
||||||
|
FirstLoginFilter firstLoginFilter,
|
||||||
|
SessionPersistentRegistry sessionRegistry,
|
||||||
|
@Autowired(required = false) GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper,
|
||||||
|
@Autowired(required = false)
|
||||||
|
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
||||||
|
@Autowired(required = false)
|
||||||
|
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.userService = userService;
|
||||||
|
this.loginEnabledValue = loginEnabledValue;
|
||||||
|
this.runningProOrHigher = runningProOrHigher;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.userAuthenticationFilter = userAuthenticationFilter;
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
this.firstLoginFilter = firstLoginFilter;
|
||||||
|
this.sessionRegistry = sessionRegistry;
|
||||||
|
this.persistentLoginRepository = persistentLoginRepository;
|
||||||
|
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
|
||||||
|
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
|
||||||
|
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
|
||||||
|
http.csrf(csrf -> csrf.disable());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginEnabledValue) {
|
||||||
|
http.addFilterBefore(
|
||||||
|
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||||
|
CookieCsrfTokenRepository cookieRepo =
|
||||||
|
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||||
|
CsrfTokenRequestAttributeHandler requestHandler =
|
||||||
|
new CsrfTokenRequestAttributeHandler();
|
||||||
|
requestHandler.setCsrfRequestAttributeName(null);
|
||||||
|
http.csrf(
|
||||||
|
csrf ->
|
||||||
|
csrf.ignoringRequestMatchers(
|
||||||
|
request -> {
|
||||||
|
String apiKey = request.getHeader("X-API-KEY");
|
||||||
|
// If there's no API key, don't ignore CSRF
|
||||||
|
// (return false)
|
||||||
|
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Validate API key using existing UserService
|
||||||
|
try {
|
||||||
|
Optional<User> user =
|
||||||
|
userService.getUserByApiKey(apiKey);
|
||||||
|
// If API key is valid, ignore CSRF (return
|
||||||
|
// true)
|
||||||
|
// If API key is invalid, don't ignore CSRF
|
||||||
|
// (return false)
|
||||||
|
return user.isPresent();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// If there's any error validating the API
|
||||||
|
// key, don't ignore CSRF
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.csrfTokenRepository(cookieRepo)
|
||||||
|
.csrfTokenRequestHandler(requestHandler));
|
||||||
|
}
|
||||||
|
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
|
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
http.sessionManagement(
|
||||||
|
sessionManagement ->
|
||||||
|
sessionManagement
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||||
|
.maximumSessions(10)
|
||||||
|
.maxSessionsPreventsLogin(false)
|
||||||
|
.sessionRegistry(sessionRegistry)
|
||||||
|
.expiredUrl("/login?logout=true"));
|
||||||
|
http.authenticationProvider(daoAuthenticationProvider());
|
||||||
|
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
|
||||||
|
http.logout(
|
||||||
|
logout ->
|
||||||
|
logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
|
||||||
|
.logoutSuccessHandler(
|
||||||
|
new CustomLogoutSuccessHandler(applicationProperties))
|
||||||
|
.clearAuthentication(true)
|
||||||
|
.invalidateHttpSession(true)
|
||||||
|
.deleteCookies("JSESSIONID", "remember-me"));
|
||||||
|
http.rememberMe(
|
||||||
|
rememberMeConfigurer -> // Use the configurator directly
|
||||||
|
rememberMeConfigurer
|
||||||
|
.tokenRepository(persistentTokenRepository())
|
||||||
|
.tokenValiditySeconds( // 14 days
|
||||||
|
14 * 24 * 60 * 60)
|
||||||
|
.userDetailsService( // Your existing UserDetailsService
|
||||||
|
userDetailsService)
|
||||||
|
.useSecureCookie( // Enable secure cookie
|
||||||
|
true)
|
||||||
|
.rememberMeParameter( // Form parameter name
|
||||||
|
"remember-me")
|
||||||
|
.rememberMeCookieName( // Cookie name
|
||||||
|
"remember-me")
|
||||||
|
.alwaysRemember(false));
|
||||||
|
http.authorizeHttpRequests(
|
||||||
|
authz ->
|
||||||
|
authz.requestMatchers(
|
||||||
|
req -> {
|
||||||
|
String uri = req.getRequestURI();
|
||||||
|
String contextPath = req.getContextPath();
|
||||||
|
// Remove the context path from the URI
|
||||||
|
String trimmedUri =
|
||||||
|
uri.startsWith(contextPath)
|
||||||
|
? uri.substring(
|
||||||
|
contextPath.length())
|
||||||
|
: uri;
|
||||||
|
return trimmedUri.startsWith("/login")
|
||||||
|
|| trimmedUri.startsWith("/oauth")
|
||||||
|
|| trimmedUri.startsWith("/saml2")
|
||||||
|
|| trimmedUri.endsWith(".svg")
|
||||||
|
|| trimmedUri.startsWith("/register")
|
||||||
|
|| trimmedUri.startsWith("/error")
|
||||||
|
|| trimmedUri.startsWith("/images/")
|
||||||
|
|| trimmedUri.startsWith("/public/")
|
||||||
|
|| trimmedUri.startsWith("/css/")
|
||||||
|
|| trimmedUri.startsWith("/fonts/")
|
||||||
|
|| trimmedUri.startsWith("/js/")
|
||||||
|
|| trimmedUri.startsWith(
|
||||||
|
"/api/v1/info/status");
|
||||||
|
})
|
||||||
|
.permitAll()
|
||||||
|
.anyRequest()
|
||||||
|
.authenticated());
|
||||||
|
// Handle User/Password Logins
|
||||||
|
if (applicationProperties.getSecurity().isUserPass()) {
|
||||||
|
http.formLogin(
|
||||||
|
formLogin ->
|
||||||
|
formLogin
|
||||||
|
.loginPage("/login")
|
||||||
|
.successHandler(
|
||||||
|
new CustomAuthenticationSuccessHandler(
|
||||||
|
loginAttemptService, userService))
|
||||||
|
.failureHandler(
|
||||||
|
new CustomAuthenticationFailureHandler(
|
||||||
|
loginAttemptService, userService))
|
||||||
|
.defaultSuccessUrl("/")
|
||||||
|
.permitAll());
|
||||||
|
}
|
||||||
|
// Handle OAUTH2 Logins
|
||||||
|
if (applicationProperties.getSecurity().isOauth2Active()) {
|
||||||
|
http.oauth2Login(
|
||||||
|
oauth2 ->
|
||||||
|
oauth2.loginPage("/oauth2")
|
||||||
|
.
|
||||||
|
/*
|
||||||
|
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||||
|
If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser'
|
||||||
|
is set as true, else login fails with an error message advising the same.
|
||||||
|
*/
|
||||||
|
successHandler(
|
||||||
|
new CustomOAuth2AuthenticationSuccessHandler(
|
||||||
|
loginAttemptService,
|
||||||
|
applicationProperties,
|
||||||
|
userService))
|
||||||
|
.failureHandler(
|
||||||
|
new CustomOAuth2AuthenticationFailureHandler())
|
||||||
|
. // Add existing Authorities from the database
|
||||||
|
userInfoEndpoint(
|
||||||
|
userInfoEndpoint ->
|
||||||
|
userInfoEndpoint
|
||||||
|
.oidcUserService(
|
||||||
|
new CustomOAuth2UserService(
|
||||||
|
applicationProperties,
|
||||||
|
userService,
|
||||||
|
loginAttemptService))
|
||||||
|
.userAuthoritiesMapper(
|
||||||
|
oAuth2userAuthoritiesMapper))
|
||||||
|
.permitAll());
|
||||||
|
}
|
||||||
|
// Handle SAML
|
||||||
|
if (applicationProperties.getSecurity().isSaml2Active() && runningProOrHigher) {
|
||||||
|
// Configure the authentication provider
|
||||||
|
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||||
|
new OpenSaml4AuthenticationProvider();
|
||||||
|
authenticationProvider.setResponseAuthenticationConverter(
|
||||||
|
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||||
|
http.authenticationProvider(authenticationProvider)
|
||||||
|
.saml2Login(
|
||||||
|
saml2 -> {
|
||||||
|
try {
|
||||||
|
saml2.loginPage("/saml2")
|
||||||
|
.relyingPartyRegistrationRepository(
|
||||||
|
saml2RelyingPartyRegistrations)
|
||||||
|
.authenticationManager(
|
||||||
|
new ProviderManager(authenticationProvider))
|
||||||
|
.successHandler(
|
||||||
|
new CustomSaml2AuthenticationSuccessHandler(
|
||||||
|
loginAttemptService,
|
||||||
|
applicationProperties,
|
||||||
|
userService))
|
||||||
|
.failureHandler(
|
||||||
|
new CustomSaml2AuthenticationFailureHandler())
|
||||||
|
.authenticationRequestResolver(
|
||||||
|
saml2AuthenticationRequestResolver);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error configuring SAML 2 login", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("SAML 2 login is not enabled. Using default.");
|
||||||
|
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
|
}
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||||
|
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||||
|
provider.setUserDetailsService(userDetailsService);
|
||||||
|
provider.setPasswordEncoder(passwordEncoder());
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public IPRateLimitingFilter rateLimitingFilter() {
|
||||||
|
// Example limit TODO add config level
|
||||||
|
int maxRequestsPerIp = 1000000;
|
||||||
|
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PersistentTokenRepository persistentTokenRepository() {
|
||||||
|
return new JPATokenRepositoryImpl(persistentLoginRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public boolean activeSecurity() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package stirling.software.SPDF.model.api.converters;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ConvertToImageRequest extends PDFWithPageNums {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The output image format",
|
||||||
|
defaultValue = "png",
|
||||||
|
allowableValues = {"png", "jpeg", "jpg", "gif", "webp"},
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String imageFormat;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"Choose between a single image containing all pages or separate images for each"
|
||||||
|
+ " page",
|
||||||
|
defaultValue = "multiple",
|
||||||
|
allowableValues = {"single", "multiple"},
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String singleOrMultiple;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The color type of the output image(s)",
|
||||||
|
defaultValue = "color",
|
||||||
|
allowableValues = {"color", "greyscale", "blackwhite"},
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String colorType;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The DPI (dots per inch) for the output image(s)",
|
||||||
|
defaultValue = "300",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Integer dpi;
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package stirling.software.SPDF.model.api.filter;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFComparison;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class FileSizeRequest extends PDFComparison {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Size of the file in bytes",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
defaultValue = "0")
|
||||||
|
private long fileSize;
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package stirling.software.SPDF.model.api.filter;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFComparison;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class PageRotationRequest extends PDFComparison {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Rotation in degrees",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
defaultValue = "0")
|
||||||
|
private int rotation;
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package stirling.software.SPDF.model.api.filter;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFComparison;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class PageSizeRequest extends PDFComparison {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Standard Page Size",
|
||||||
|
allowableValues = {"A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL"},
|
||||||
|
defaultValue = "A4",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String standardPageSize;
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package stirling.software.SPDF.model.api.general;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class OverlayPdfsRequest extends PDFFile {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"An array of PDF files to be used as overlays on the base PDF. The order in"
|
||||||
|
+ " these files is applied based on the selected mode.",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private MultipartFile[] overlayFiles;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"The mode of overlaying: 'SequentialOverlay' for sequential application,"
|
||||||
|
+ " 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay'"
|
||||||
|
+ " for fixed repetition based on provided counts",
|
||||||
|
allowableValues = {"SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"},
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String overlayMode;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"An array of integers specifying the number of times each corresponding overlay"
|
||||||
|
+ " file should be applied in the 'FixedRepeatOverlay' mode. This should"
|
||||||
|
+ " match the length of the overlayFiles array.",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
|
private int[] counts;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Overlay position 0 is Foregound, 1 is Background",
|
||||||
|
allowableValues = {"0", "1"},
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
type = "number")
|
||||||
|
private int overlayPosition;
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package stirling.software.SPDF.model.api.misc;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class AddStampRequest extends PDFWithPageNums {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The stamp type (text or image)",
|
||||||
|
allowableValues = {"text", "image"},
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String stampType;
|
||||||
|
|
||||||
|
@Schema(description = "The stamp text", defaultValue = "Stirling Software")
|
||||||
|
private String stampText;
|
||||||
|
|
||||||
|
@Schema(description = "The stamp image")
|
||||||
|
private MultipartFile stampImage;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The selected alphabet of the stamp text",
|
||||||
|
allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"},
|
||||||
|
defaultValue = "roman")
|
||||||
|
private String alphabet = "roman";
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The font size of the stamp text and image",
|
||||||
|
defaultValue = "30",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private float fontSize;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The rotation of the stamp in degrees",
|
||||||
|
defaultValue = "0",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private float rotation;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The opacity of the stamp (0.0 - 1.0)",
|
||||||
|
defaultValue = "0.5",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private float opacity;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"Position for stamp placement based on a 1-9 grid (1: bottom-left, 2: bottom-center,"
|
||||||
|
+ " 3: bottom-right, 4: middle-left, 5: middle-center, 6: middle-right,"
|
||||||
|
+ " 7: top-left, 8: top-center, 9: top-right)",
|
||||||
|
allowableValues = {"1", "2", "3", "4", "5", "6", "7", "8", "9"},
|
||||||
|
defaultValue = "5",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private int position;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"Override X coordinate for stamp placement. If set, it will override the"
|
||||||
|
+ " position-based calculation. Negative value means no override.",
|
||||||
|
defaultValue = "-1",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private float overrideX; // Default to -1 indicating no override
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"Override Y coordinate for stamp placement. If set, it will override the"
|
||||||
|
+ " position-based calculation. Negative value means no override.",
|
||||||
|
defaultValue = "-1",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private float overrideY; // Default to -1 indicating no override
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Specifies the margin size for the stamp.",
|
||||||
|
allowableValues = {"small", "medium", "large", "x-large"},
|
||||||
|
defaultValue = "medium",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String customMargin;
|
||||||
|
|
||||||
|
@Schema(description = "The color of the stamp text", defaultValue = "#d3d3d3")
|
||||||
|
private String customColor;
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package stirling.software.SPDF.model.api.misc;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class MetadataRequest extends PDFFile {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Delete all metadata if set to true",
|
||||||
|
defaultValue = "false",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Boolean deleteAll;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The author of the document",
|
||||||
|
defaultValue = "author",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
|
private String author;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)",
|
||||||
|
pattern = "yyyy/MM/dd HH:mm:ss",
|
||||||
|
defaultValue = "2023/10/01 12:00:00",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
|
private String creationDate;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The creator of the document",
|
||||||
|
defaultValue = "creator",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
|
private String creator;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The keywords for the document",
|
||||||
|
defaultValue = "keywords",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
|
private String keywords;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)",
|
||||||
|
pattern = "yyyy/MM/dd HH:mm:ss",
|
||||||
|
defaultValue = "2023/10/01 12:00:00",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
|
private String modificationDate;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The producer of the document",
|
||||||
|
defaultValue = "producer",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
|
private String producer;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The subject of the document",
|
||||||
|
defaultValue = "subject",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
|
private String subject;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The title of the document",
|
||||||
|
defaultValue = "title",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The trapped status of the document",
|
||||||
|
defaultValue = "False",
|
||||||
|
allowableValues = {"True", "False", "Unknown"},
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
|
private String trapped;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"Map list of key and value of custom parameters. Note these must start with"
|
||||||
|
+ " customKey and customValue if they are non-standard")
|
||||||
|
private Map<String, String> allRequestParams;
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
package stirling.software.SPDF.model.api.security;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class AddPasswordRequest extends PDFFile {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"The owner password to be added to the PDF file (Restricts what can be done"
|
||||||
|
+ " with the document once it is opened)",
|
||||||
|
format = "password")
|
||||||
|
private String ownerPassword;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"The password to be added to the PDF file (Restricts the opening of the"
|
||||||
|
+ " document itself.)",
|
||||||
|
format = "password")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The length of the encryption key",
|
||||||
|
allowableValues = {"40", "128", "256"},
|
||||||
|
defaultValue = "256",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private int keyLength = 256;
|
||||||
|
|
||||||
|
@Schema(description = "Whether document assembly is prevented", defaultValue = "false")
|
||||||
|
private Boolean preventAssembly;
|
||||||
|
|
||||||
|
@Schema(description = "Whether content extraction is prevented", defaultValue = "false")
|
||||||
|
private Boolean preventExtractContent;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Whether content extraction for accessibility is prevented",
|
||||||
|
defaultValue = "false")
|
||||||
|
private Boolean preventExtractForAccessibility;
|
||||||
|
|
||||||
|
@Schema(description = "Whether form filling is prevented", defaultValue = "false")
|
||||||
|
private Boolean preventFillInForm;
|
||||||
|
|
||||||
|
@Schema(description = "Whether document modification is prevented", defaultValue = "false")
|
||||||
|
private Boolean preventModify;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Whether modification of annotations is prevented",
|
||||||
|
defaultValue = "false")
|
||||||
|
private Boolean preventModifyAnnotations;
|
||||||
|
|
||||||
|
@Schema(description = "Whether printing of the document is prevented", defaultValue = "false")
|
||||||
|
private Boolean preventPrinting;
|
||||||
|
|
||||||
|
@Schema(description = "Whether faithful printing is prevented", defaultValue = "false")
|
||||||
|
private Boolean preventPrintingFaithful;
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package stirling.software.SPDF.model.api.security;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ManualRedactPdfRequest extends PDFWithPageNums {
|
||||||
|
@Schema(
|
||||||
|
description = "A list of areas that should be redacted",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private List<RedactionArea> redactions;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Convert the redacted PDF to an image",
|
||||||
|
defaultValue = "false",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Boolean convertPDFToImage;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The color used to fully redact certain pages",
|
||||||
|
defaultValue = "#000000",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String pageRedactionColor;
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package stirling.software.SPDF.model.api.security;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class SanitizePdfRequest extends PDFFile {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Remove JavaScript actions from the PDF",
|
||||||
|
defaultValue = "true",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Boolean removeJavaScript;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Remove embedded files from the PDF",
|
||||||
|
defaultValue = "true",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Boolean removeEmbeddedFiles;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Remove XMP metadata from the PDF",
|
||||||
|
defaultValue = "false",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Boolean removeXMPMetadata;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Remove document info metadata from the PDF",
|
||||||
|
defaultValue = "false",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Boolean removeMetadata;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Remove links from the PDF",
|
||||||
|
defaultValue = "false",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Boolean removeLinks;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Remove fonts from the PDF",
|
||||||
|
defaultValue = "false",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Boolean removeFonts;
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package stirling.software.SPDF.utils;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class RequestUriUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIsStaticResource() {
|
||||||
|
assertTrue(RequestUriUtils.isStaticResource("/css/styles.css"));
|
||||||
|
assertTrue(RequestUriUtils.isStaticResource("/js/script.js"));
|
||||||
|
assertTrue(RequestUriUtils.isStaticResource("/images/logo.png"));
|
||||||
|
assertTrue(RequestUriUtils.isStaticResource("/public/index.html"));
|
||||||
|
assertTrue(RequestUriUtils.isStaticResource("/pdfjs/pdf.worker.js"));
|
||||||
|
assertTrue(RequestUriUtils.isStaticResource("/api/v1/info/status"));
|
||||||
|
assertTrue(RequestUriUtils.isStaticResource("/some-path/icon.svg"));
|
||||||
|
assertFalse(RequestUriUtils.isStaticResource("/api/v1/users"));
|
||||||
|
assertFalse(RequestUriUtils.isStaticResource("/api/v1/orders"));
|
||||||
|
assertFalse(RequestUriUtils.isStaticResource("/"));
|
||||||
|
assertTrue(RequestUriUtils.isStaticResource("/login"));
|
||||||
|
assertFalse(RequestUriUtils.isStaticResource("/register"));
|
||||||
|
assertFalse(RequestUriUtils.isStaticResource("/api/v1/products"));
|
||||||
|
}
|
||||||
|
}
|
@ -22,13 +22,13 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label th:text="#{PDFToText.selectText.1}"></label>
|
<label th:text="#{PDFToText.selectText.1}"></label>
|
||||||
<select class="form-control" name="outputFormat">
|
<select class="form-control" name="outputFormat">
|
||||||
<option th:if="${@endpointConfiguration.isEndpointEnabled('pdf-to-rtf')}" value="rtf">RTF</option>
|
<option th:if="${@endpointConfigurationService.isEndpointEnabled('pdf-to-rtf')}" value="rtf">RTF</option>
|
||||||
<option value="txt">TXT</option>
|
<option value="txt">TXT</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToText.submit}"></button>
|
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToText.submit}"></button>
|
||||||
</form>
|
</form>
|
||||||
<p th:if="${@endpointConfiguration.isEndpointEnabled('pdf-to-rtf')}" class="mt-3" th:text="#{PDFToText.credit}"></p>
|
<p th:if="${@endpointConfigurationService.isEndpointEnabled('pdf-to-rtf')}" class="mt-3" th:text="#{PDFToText.credit}"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div th:fragment="card" class="feature-card hidden" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)} "
|
<div th:fragment="card" class="feature-card hidden" th:id="${id}" th:if="${@endpointConfigurationService.isEndpointEnabled(cardLink)} "
|
||||||
th:data-bs-tags="${tags}"
|
th:data-bs-tags="${tags}"
|
||||||
th:data-bs-link="@{${endpoint}}">
|
th:data-bs-link="@{${endpoint}}">
|
||||||
<a th:href="${cardLink}">
|
<a th:href="${cardLink}">
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('multi-tool')}">
|
<li class="nav-item" th:if="${@endpointConfigurationService.isEndpointEnabled('multi-tool')}">
|
||||||
<a class="nav-link" href="#" th:href="@{'/multi-tool'}"
|
<a class="nav-link" href="#" th:href="@{'/multi-tool'}"
|
||||||
th:classappend="${currentPage}=='multi-tool' ? 'active' : ''" th:title="#{home.multiTool.desc}">
|
th:classappend="${currentPage}=='multi-tool' ? 'active' : ''" th:title="#{home.multiTool.desc}">
|
||||||
<span class="material-symbols-rounded">
|
<span class="material-symbols-rounded">
|
||||||
@ -67,7 +67,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('pipeline')}">
|
<li class="nav-item" th:if="${@endpointConfigurationService.isEndpointEnabled('pipeline')}">
|
||||||
<a class="nav-link" href="#" th:href="@{'/pipeline'}"
|
<a class="nav-link" href="#" th:href="@{'/pipeline'}"
|
||||||
th:classappend="${currentPage}=='pipeline' ? 'active' : ''" th:title="#{home.pipeline.desc}">
|
th:classappend="${currentPage}=='pipeline' ? 'active' : ''" th:title="#{home.pipeline.desc}">
|
||||||
<span class="material-symbols-rounded">
|
<span class="material-symbols-rounded">
|
||||||
@ -77,7 +77,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('compress-pdf')}">
|
<li class="nav-item" th:if="${@endpointConfigurationService.isEndpointEnabled('compress-pdf')}">
|
||||||
<a class="nav-link" href="#" title="#{home.compressPdfs.title}" th:href="@{'/compress-pdf'}"
|
<a class="nav-link" href="#" title="#{home.compressPdfs.title}" th:href="@{'/compress-pdf'}"
|
||||||
th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:title="#{home.compressPdfs.desc}">
|
th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:title="#{home.compressPdfs.desc}">
|
||||||
<span class="material-symbols-rounded">
|
<span class="material-symbols-rounded">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<th:block th:fragment="navbarEntry(endpoint, toolIcon, titleKey, descKey, tagKey, toolGroup)"
|
<th:block th:fragment="navbarEntry(endpoint, toolIcon, titleKey, descKey, tagKey, toolGroup)"
|
||||||
th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}">
|
th:if="${@endpointConfigurationService.isEndpointEnabled(endpoint)}">
|
||||||
<a th:id="@{${endpoint}}" class="dropdown-item" style="position:relative" th:href="@{${endpoint}}"
|
<a th:id="@{${endpoint}}" class="dropdown-item" style="position:relative" th:href="@{${endpoint}}"
|
||||||
th:data-bs-link="@{${endpoint}}"
|
th:data-bs-link="@{${endpoint}}"
|
||||||
th:classappend="${endpoint.equals(currentPage)} ? ${toolGroup} + ' active' : '' + ${toolGroup}"
|
th:classappend="${endpoint.equals(currentPage)} ? ${toolGroup} + ' active' : '' + ${toolGroup}"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<th:block th:fragment="navbarEntry(endpoint, toolIcon, titleKey, descKey, tagKey, toolGroup)"
|
<th:block th:fragment="navbarEntry(endpoint, toolIcon, titleKey, descKey, tagKey, toolGroup)"
|
||||||
th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}">
|
th:if="${@endpointConfigurationService.isEndpointEnabled(endpoint)}">
|
||||||
<a th:id="@{${endpoint}}" class="dropdown-item" style="position:relative" th:href="@{${endpoint}}"
|
<a th:id="@{${endpoint}}" class="dropdown-item" style="position:relative" th:href="@{${endpoint}}"
|
||||||
th:data-bs-link="@{${endpoint}}"
|
th:data-bs-link="@{${endpoint}}"
|
||||||
th:classappend="${endpoint.equals(currentPage)} ? ${toolGroup} + ' active' : '' + ${toolGroup}"
|
th:classappend="${endpoint.equals(currentPage)} ? ${toolGroup} + ' active' : '' + ${toolGroup}"
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="page-container">
|
<div id="page-container">
|
||||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
<div style="transform-origin: top;"
|
<div style="transform-origin: top;"
|
||||||
id="scale-wrap">
|
id="scale-wrap">
|
||||||
@ -125,11 +125,11 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Survey Modal -->
|
<!-- Survey Modal -->
|
||||||
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel"
|
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -165,11 +165,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Analytics Modal -->
|
<!-- Analytics Modal -->
|
||||||
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel"
|
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel"
|
||||||
aria-hidden="true" th:if="${@analyticsPrompt}">
|
aria-hidden="true" th:if="${@analyticsPrompt}">
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -194,10 +194,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.favorite-icon {
|
.favorite-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 0rem;
|
width: 0rem;
|
||||||
@ -211,17 +211,17 @@
|
|||||||
.toggle-favourites.active {
|
.toggle-favourites.active {
|
||||||
color: gold;
|
color: gold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
/*<![CDATA[*/
|
/*<![CDATA[*/
|
||||||
window.analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
|
window.analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
|
||||||
/*]]>*/
|
/*]]>*/
|
||||||
|
|
||||||
window.showSurvey = /*[[${showSurveyFromDocker}]]*/ true
|
window.showSurvey = /*[[${showSurveyFromDocker}]]*/ true
|
||||||
</script>
|
</script>
|
||||||
<script th:src="@{'/js/pages/home.js'}" th:inline="javascript"></script>
|
<script th:src="@{'/js/pages/home.js'}" th:inline="javascript"></script>
|
||||||
<script>
|
<script>
|
||||||
function applyScale() {
|
function applyScale() {
|
||||||
const baseWidth = 1440;
|
const baseWidth = 1440;
|
||||||
const baseHeight = 1000;
|
const baseHeight = 1000;
|
||||||
@ -234,7 +234,7 @@
|
|||||||
|
|
||||||
window.addEventListener('resize', applyScale);
|
window.addEventListener('resize', applyScale);
|
||||||
window.addEventListener('load', applyScale);
|
window.addEventListener('load', applyScale);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
@ -26,15 +26,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
registry.addResourceHandler("/**")
|
registry.addResourceHandler("/**")
|
||||||
.addResourceLocations(
|
.addResourceLocations(
|
||||||
"file:" + InstallationPathConfig.getStaticPath(),
|
"file:" + InstallationPathConfig.getStaticPath(),
|
||||||
"classpath:/static/",
|
"classpath:/static/"
|
||||||
"classpath:/templates/",
|
|
||||||
"classpath:/stirling-pdf/",
|
|
||||||
"classpath:/stirling-pdf/static/",
|
|
||||||
"classpath:/stirling-pdf/templates/",
|
|
||||||
"/static/",
|
|
||||||
"/stirling-pdf/",
|
|
||||||
"/stirling-pdf/static/",
|
|
||||||
"/stirling-pdf/templates/"
|
|
||||||
);
|
);
|
||||||
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
|
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
|
||||||
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
|
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
|
||||||
|
@ -0,0 +1,163 @@
|
|||||||
|
package stirling.software.spdf.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
import stirling.software.spdf.model.api.PDFWithPageNums;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Slf4j
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SplitPDFController {
|
||||||
|
|
||||||
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/split-pages")
|
||||||
|
@Operation(
|
||||||
|
summary = "Split a PDF file into separate documents",
|
||||||
|
description =
|
||||||
|
"This endpoint splits a given PDF file into separate documents based on the"
|
||||||
|
+ " specified page numbers or ranges. Users can specify pages using"
|
||||||
|
+ " individual numbers, ranges, or 'all' for every page. Input:PDF"
|
||||||
|
+ " Output:PDF Type:SIMO")
|
||||||
|
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
PDDocument document = null;
|
||||||
|
Path zipFile = null;
|
||||||
|
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
String pages = request.getPageNumbers();
|
||||||
|
// open the pdf document
|
||||||
|
|
||||||
|
document = pdfDocumentFactory.load(file);
|
||||||
|
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
|
||||||
|
int totalPages = document.getNumberOfPages();
|
||||||
|
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
|
||||||
|
if (!pageNumbers.contains(totalPages - 1)) {
|
||||||
|
// Create a mutable ArrayList so we can add to it
|
||||||
|
pageNumbers = new ArrayList<>(pageNumbers);
|
||||||
|
pageNumbers.add(totalPages - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Splitting PDF into pages: {}",
|
||||||
|
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||||
|
|
||||||
|
// split the document
|
||||||
|
splitDocumentsBoas = new ArrayList<>();
|
||||||
|
int previousPageNumber = 0;
|
||||||
|
for (int splitPoint : pageNumbers) {
|
||||||
|
try (PDDocument splitDocument =
|
||||||
|
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) {
|
||||||
|
for (int i = previousPageNumber; i <= splitPoint; i++) {
|
||||||
|
PDPage page = document.getPage(i);
|
||||||
|
splitDocument.addPage(page);
|
||||||
|
log.info("Adding page {} to split document", i);
|
||||||
|
}
|
||||||
|
previousPageNumber = splitPoint + 1;
|
||||||
|
|
||||||
|
// Transfer metadata to split pdf
|
||||||
|
// PdfMetadataService.setMetadataToPdf(splitDocument, metadata);
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
splitDocument.save(baos);
|
||||||
|
|
||||||
|
splitDocumentsBoas.add(baos);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed splitting documents and saving them", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// closing the original document
|
||||||
|
document.close();
|
||||||
|
|
||||||
|
zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
|
|
||||||
|
String filename =
|
||||||
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "");
|
||||||
|
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||||
|
// loop through the split documents and write them to the zip file
|
||||||
|
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
||||||
|
String fileName = filename + "_" + (i + 1) + ".pdf";
|
||||||
|
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
|
||||||
|
byte[] pdf = baos.toByteArray();
|
||||||
|
|
||||||
|
// Add PDF file to the zip
|
||||||
|
ZipEntry pdfEntry = new ZipEntry(fileName);
|
||||||
|
zipOut.putNextEntry(pdfEntry);
|
||||||
|
zipOut.write(pdf);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
|
||||||
|
log.info("Wrote split document {} to zip file", fileName);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed writing to zip", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Successfully created zip file with split documents: {}", zipFile.toString());
|
||||||
|
byte[] data = Files.readAllBytes(zipFile);
|
||||||
|
Files.deleteIfExists(zipFile);
|
||||||
|
|
||||||
|
// return the Resource in the response
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
// Close the main document
|
||||||
|
if (document != null) {
|
||||||
|
document.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all ByteArrayOutputStreams
|
||||||
|
for (ByteArrayOutputStream baos : splitDocumentsBoas) {
|
||||||
|
if (baos != null) {
|
||||||
|
baos.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete temporary zip file
|
||||||
|
if (zipFile != null) {
|
||||||
|
Files.deleteIfExists(zipFile);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error while cleaning up resources", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -31,11 +31,11 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.spdf.model.api.SplitPdfByChaptersRequest;
|
|
||||||
import stirling.software.common.model.PdfMetadata;
|
import stirling.software.common.model.PdfMetadata;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.service.PdfMetadataService;
|
import stirling.software.common.service.PdfMetadataService;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
import stirling.software.spdf.model.api.SplitPdfByChaptersRequest;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/general")
|
@RequestMapping("/api/v1/general")
|
||||||
|
@ -31,9 +31,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import stirling.software.spdf.model.api.SplitPdfBySectionsRequest;
|
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
import stirling.software.spdf.model.api.SplitPdfBySectionsRequest;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/general")
|
@RequestMapping("/api/v1/general")
|
||||||
|
@ -0,0 +1,311 @@
|
|||||||
|
package stirling.software.spdf.controller.api.misc;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.geom.Ellipse2D;
|
||||||
|
import java.awt.geom.Path2D;
|
||||||
|
import java.awt.image.*;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.common.model.api.PDFFile;
|
||||||
|
import stirling.software.common.util.PdfUtils;
|
||||||
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/misc")
|
||||||
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
|
public class FakeScanControllerWIP {
|
||||||
|
|
||||||
|
// TODO finish
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/fake-scan")
|
||||||
|
@Hidden
|
||||||
|
@Operation(
|
||||||
|
summary = "Repair a PDF file",
|
||||||
|
description =
|
||||||
|
"This endpoint repairs a given PDF file by running qpdf command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.")
|
||||||
|
public ResponseEntity<byte[]> fakeScan(@ModelAttribute PDFFile request) throws IOException {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
|
||||||
|
// Load the PDF document
|
||||||
|
PDDocument document = Loader.loadPDF(inputFile.getBytes());
|
||||||
|
PDFRenderer renderer = new PDFRenderer(document);
|
||||||
|
List<BufferedImage> images = new ArrayList<>();
|
||||||
|
// Convert each page to an image
|
||||||
|
for (int i = 0; i < document.getNumberOfPages(); i++) {
|
||||||
|
BufferedImage image = renderer.renderImageWithDPI(i, 150, ImageType.GRAY);
|
||||||
|
images.add(processImage(image));
|
||||||
|
}
|
||||||
|
document.close();
|
||||||
|
|
||||||
|
// Create a new PDF document with the processed images
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
PDDocument newDocument = new PDDocument();
|
||||||
|
for (BufferedImage img : images) {
|
||||||
|
// PDPageContentStream contentStream = new PDPageContentStream(newDocument, new
|
||||||
|
// PDPage());
|
||||||
|
PDImageXObject pdImage = JPEGFactory.createFromImage(newDocument, img);
|
||||||
|
PdfUtils.addImageToDocument(newDocument, pdImage, "maintainAspectRatio", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
newDocument.save(baos);
|
||||||
|
newDocument.close();
|
||||||
|
|
||||||
|
// Return the optimized PDF as a response
|
||||||
|
String outputFilename =
|
||||||
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_scanned.pdf";
|
||||||
|
return WebResponseUtils.boasToWebResponse(baos, outputFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BufferedImage processImage(BufferedImage image) {
|
||||||
|
// Rotation
|
||||||
|
|
||||||
|
image = softenEdges(image, 50);
|
||||||
|
image = rotate(image, 1);
|
||||||
|
|
||||||
|
image = applyGaussianBlur(image, 0.5);
|
||||||
|
addGaussianNoise(image, 0.5);
|
||||||
|
image = linearStretch(image);
|
||||||
|
addDustAndHairs(image, 3);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage rotate(BufferedImage image, double rotation) {
|
||||||
|
|
||||||
|
double rotationRequired = Math.toRadians(rotation);
|
||||||
|
double locationX = (double) image.getWidth() / 2;
|
||||||
|
double locationY = (double) image.getHeight() / 2;
|
||||||
|
AffineTransform tx =
|
||||||
|
AffineTransform.getRotateInstance(rotationRequired, locationX, locationY);
|
||||||
|
AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BICUBIC);
|
||||||
|
return op.filter(image, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage applyGaussianBlur(BufferedImage image, double sigma) {
|
||||||
|
int radius = 3; // Fixed radius size for simplicity
|
||||||
|
|
||||||
|
int size = 2 * radius + 1;
|
||||||
|
float[] data = new float[size * size];
|
||||||
|
double sum = 0.0;
|
||||||
|
|
||||||
|
for (int i = -radius; i <= radius; i++) {
|
||||||
|
for (int j = -radius; j <= radius; j++) {
|
||||||
|
double xDistance = (double) i * i;
|
||||||
|
double yDistance = (double) j * j;
|
||||||
|
double g = Math.exp(-(xDistance + yDistance) / (2 * sigma * sigma));
|
||||||
|
data[(i + radius) * size + j + radius] = (float) g;
|
||||||
|
sum += g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the kernel
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
if (sum != 0) data[i] /= sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
Kernel kernel = new Kernel(size, size, data);
|
||||||
|
BufferedImageOp op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
|
||||||
|
return op.filter(image, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BufferedImage softenEdges(BufferedImage image, int featherRadius) {
|
||||||
|
int width = image.getWidth();
|
||||||
|
int height = image.getHeight();
|
||||||
|
BufferedImage output = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
|
||||||
|
Graphics2D g2 = output.createGraphics();
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||||
|
g2.setRenderingHint(
|
||||||
|
RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
|
||||||
|
|
||||||
|
g2.drawImage(image, 0, 0, null);
|
||||||
|
g2.setComposite(AlphaComposite.DstIn);
|
||||||
|
|
||||||
|
// Top edge
|
||||||
|
g2.setPaint(
|
||||||
|
new GradientPaint(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
new Color(0, 0, 0, 1f),
|
||||||
|
0,
|
||||||
|
featherRadius * 2f,
|
||||||
|
new Color(0, 0, 0, 0f)));
|
||||||
|
g2.fillRect(0, 0, width, featherRadius);
|
||||||
|
|
||||||
|
// Bottom edge
|
||||||
|
g2.setPaint(
|
||||||
|
new GradientPaint(
|
||||||
|
0,
|
||||||
|
height - featherRadius * 2f,
|
||||||
|
new Color(0, 0, 0, 0f),
|
||||||
|
0,
|
||||||
|
height,
|
||||||
|
new Color(0, 0, 0, 1f)));
|
||||||
|
g2.fillRect(0, height - featherRadius, width, featherRadius);
|
||||||
|
|
||||||
|
// Left edge
|
||||||
|
g2.setPaint(
|
||||||
|
new GradientPaint(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
new Color(0, 0, 0, 1f),
|
||||||
|
featherRadius * 2f,
|
||||||
|
0,
|
||||||
|
new Color(0, 0, 0, 0f)));
|
||||||
|
g2.fillRect(0, 0, featherRadius, height);
|
||||||
|
|
||||||
|
// Right edge
|
||||||
|
g2.setPaint(
|
||||||
|
new GradientPaint(
|
||||||
|
width - featherRadius * 2f,
|
||||||
|
0,
|
||||||
|
new Color(0, 0, 0, 0f),
|
||||||
|
width,
|
||||||
|
0,
|
||||||
|
new Color(0, 0, 0, 1f)));
|
||||||
|
g2.fillRect(width - featherRadius, 0, featherRadius, height);
|
||||||
|
|
||||||
|
g2.dispose();
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDustAndHairs(BufferedImage image, float intensity) {
|
||||||
|
int width = image.getWidth();
|
||||||
|
int height = image.getHeight();
|
||||||
|
Graphics2D g2d = image.createGraphics();
|
||||||
|
Random random = new SecureRandom();
|
||||||
|
|
||||||
|
// Set rendering hints for better quality
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
|
||||||
|
// Calculate the number of artifacts based on intensity
|
||||||
|
int numSpots = (int) (intensity * 10);
|
||||||
|
int numHairs = (int) (intensity * 20);
|
||||||
|
|
||||||
|
// Add spots with more variable sizes
|
||||||
|
g2d.setColor(new Color(100, 100, 100, 50)); // Semi-transparent gray
|
||||||
|
for (int i = 0; i < numSpots; i++) {
|
||||||
|
int x = random.nextInt(width);
|
||||||
|
int y = random.nextInt(height);
|
||||||
|
int ovalSize = 1 + random.nextInt(3); // Base size + variable component
|
||||||
|
if (random.nextFloat() > 0.9) {
|
||||||
|
// 10% chance to get a larger spot
|
||||||
|
ovalSize += random.nextInt(3);
|
||||||
|
}
|
||||||
|
g2d.fill(new Ellipse2D.Double(x, y, ovalSize, ovalSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hairs
|
||||||
|
g2d.setStroke(new BasicStroke(0.5f)); // Thin stroke for hairs
|
||||||
|
g2d.setColor(new Color(80, 80, 80, 40)); // Slightly lighter and more transparent
|
||||||
|
for (int i = 0; i < numHairs; i++) {
|
||||||
|
int x1 = random.nextInt(width);
|
||||||
|
int y1 = random.nextInt(height);
|
||||||
|
int x2 = x1 + random.nextInt(20) - 10; // Random length and direction
|
||||||
|
int y2 = y1 + random.nextInt(20) - 10;
|
||||||
|
Path2D.Double hair = new Path2D.Double();
|
||||||
|
hair.moveTo(x1, y1);
|
||||||
|
hair.curveTo(x1, y1, (double) (x1 + x2) / 2, (double) (y1 + y2) / 2, x2, y2);
|
||||||
|
g2d.draw(hair);
|
||||||
|
}
|
||||||
|
|
||||||
|
g2d.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addGaussianNoise(BufferedImage image, double strength) {
|
||||||
|
Random rand = new SecureRandom();
|
||||||
|
int width = image.getWidth();
|
||||||
|
int height = image.getHeight();
|
||||||
|
|
||||||
|
for (int i = 0; i < width; i++) {
|
||||||
|
for (int j = 0; j < height; j++) {
|
||||||
|
int rgba = image.getRGB(i, j);
|
||||||
|
int alpha = (rgba >> 24) & 0xff;
|
||||||
|
int red = (rgba >> 16) & 0xff;
|
||||||
|
int green = (rgba >> 8) & 0xff;
|
||||||
|
int blue = rgba & 0xff;
|
||||||
|
|
||||||
|
// Apply Gaussian noise
|
||||||
|
red = (int) (red + rand.nextGaussian() * strength);
|
||||||
|
green = (int) (green + rand.nextGaussian() * strength);
|
||||||
|
blue = (int) (blue + rand.nextGaussian() * strength);
|
||||||
|
|
||||||
|
// Clamping values to the 0-255 range
|
||||||
|
red = Math.min(Math.max(0, red), 255);
|
||||||
|
green = Math.min(Math.max(0, green), 255);
|
||||||
|
blue = Math.min(Math.max(0, blue), 255);
|
||||||
|
|
||||||
|
image.setRGB(i, j, (alpha << 24) | (red << 16) | (green << 8) | blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BufferedImage linearStretch(BufferedImage image) {
|
||||||
|
int width = image.getWidth();
|
||||||
|
int height = image.getHeight();
|
||||||
|
int min = 255;
|
||||||
|
int max = 0;
|
||||||
|
|
||||||
|
// First pass: find the min and max grayscale values
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
int rgb = image.getRGB(x, y);
|
||||||
|
int gray =
|
||||||
|
(int)
|
||||||
|
(((rgb >> 16) & 0xff) * 0.299
|
||||||
|
+ ((rgb >> 8) & 0xff) * 0.587
|
||||||
|
+ (rgb & 0xff) * 0.114); // Convert to grayscale
|
||||||
|
if (gray < min) min = gray;
|
||||||
|
if (gray > max) max = gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: stretch the histogram
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
int rgb = image.getRGB(x, y);
|
||||||
|
int alpha = (rgb >> 24) & 0xff;
|
||||||
|
int red = (rgb >> 16) & 0xff;
|
||||||
|
int green = (rgb >> 8) & 0xff;
|
||||||
|
int blue = rgb & 0xff;
|
||||||
|
|
||||||
|
// Apply linear stretch to each channel
|
||||||
|
red = (int) (((red - min) / (float) (max - min)) * 255);
|
||||||
|
green = (int) (((green - min) / (float) (max - min)) * 255);
|
||||||
|
blue = (int) (((blue - min) / (float) (max - min)) * 255);
|
||||||
|
|
||||||
|
// Set new RGB value maintaining the alpha channel
|
||||||
|
rgb = (alpha << 24) | (red << 16) | (green << 8) | blue;
|
||||||
|
image.setRGB(x, y, rgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
package stirling.software.spdf.controller.web;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.spdf.model.Dependency;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class HomeWebController {
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@GetMapping("/about")
|
||||||
|
@Hidden
|
||||||
|
public String gameForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "about");
|
||||||
|
return "about";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/licenses")
|
||||||
|
@Hidden
|
||||||
|
public String licensesForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "licenses");
|
||||||
|
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
|
||||||
|
try {
|
||||||
|
InputStream is = resource.getInputStream();
|
||||||
|
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
Map<String, List<Dependency>> data =
|
||||||
|
mapper.readValue(json, new TypeReference<Map<String, List<Dependency>>>() {});
|
||||||
|
model.addAttribute("dependencies", data.get("dependencies"));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("exception", e);
|
||||||
|
}
|
||||||
|
return "licenses";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/releases")
|
||||||
|
public String getReleaseNotes(Model model) {
|
||||||
|
return "releases";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/")
|
||||||
|
public String home(Model model) {
|
||||||
|
model.addAttribute("currentPage", "home");
|
||||||
|
String showSurvey = System.getenv("SHOW_SURVEY");
|
||||||
|
boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey);
|
||||||
|
model.addAttribute("showSurveyFromDocker", showSurveyValue);
|
||||||
|
return "home";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/home")
|
||||||
|
public String root(Model model) {
|
||||||
|
return "redirect:/";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/home-legacy")
|
||||||
|
public String homeLegacy(Model model) {
|
||||||
|
model.addAttribute("currentPage", "home-legacy");
|
||||||
|
return "home-legacy";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||||
|
@ResponseBody
|
||||||
|
@Hidden
|
||||||
|
public String getRobotsTxt() {
|
||||||
|
Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility();
|
||||||
|
if (Boolean.TRUE.equals(allowGoogle)) {
|
||||||
|
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
|
||||||
|
} else {
|
||||||
|
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,12 +9,22 @@ import stirling.software.common.model.api.PDFFile;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = false)
|
@EqualsAndHashCode(callSuper = false)
|
||||||
public class SplitPdfByChaptersRequest extends PDFFile {
|
public class SplitPdfByChaptersRequest extends PDFFile {
|
||||||
@Schema(description = "Whether to include Metadata or not", example = "true")
|
@Schema(
|
||||||
|
description = "Whether to include Metadata or not",
|
||||||
|
defaultValue = "true",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private Boolean includeMetadata;
|
private Boolean includeMetadata;
|
||||||
|
|
||||||
@Schema(description = "Whether to allow duplicates or not", example = "true")
|
@Schema(
|
||||||
|
description = "Whether to allow duplicates or not",
|
||||||
|
defaultValue = "true",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private Boolean allowDuplicates;
|
private Boolean allowDuplicates;
|
||||||
|
|
||||||
@Schema(description = "Maximum bookmark level required", example = "2")
|
@Schema(
|
||||||
|
description = "Maximum bookmark level required",
|
||||||
|
minimum = "0",
|
||||||
|
defaultValue = "2",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private Integer bookmarkLevel;
|
private Integer bookmarkLevel;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@ -12,22 +12,37 @@ public class RedactPdfRequest extends PDFFile {
|
|||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description = "List of text to redact from the PDF",
|
description = "List of text to redact from the PDF",
|
||||||
type = "string",
|
defaultValue = "text,text2",
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private String listOfText;
|
private String listOfText;
|
||||||
|
|
||||||
@Schema(description = "Whether to use regex for the listOfText", defaultValue = "false")
|
@Schema(
|
||||||
private boolean useRegex;
|
description = "Whether to use regex for the listOfText",
|
||||||
|
defaultValue = "false",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Boolean useRegex;
|
||||||
|
|
||||||
@Schema(description = "Whether to use whole word search", defaultValue = "false")
|
@Schema(
|
||||||
private boolean wholeWordSearch;
|
description = "Whether to use whole word search",
|
||||||
|
defaultValue = "false",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Boolean wholeWordSearch;
|
||||||
|
|
||||||
@Schema(description = "Hexadecimal color code for redaction, e.g. #FF0000 or 000000", defaultValue = "#000000")
|
@Schema(
|
||||||
private String redactColor = "#000000";
|
description = "The color for redaction",
|
||||||
|
defaultValue = "#000000",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String redactColor;
|
||||||
|
|
||||||
@Schema(description = "Custom padding for redaction", type = "number")
|
@Schema(
|
||||||
|
description = "Custom padding for redaction",
|
||||||
|
type = "number",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private float customPadding;
|
private float customPadding;
|
||||||
|
|
||||||
@Schema(description = "Convert the redacted PDF to an image", defaultValue = "false")
|
@Schema(
|
||||||
private boolean convertPDFToImage;
|
description = "Convert the redacted PDF to an image",
|
||||||
|
defaultValue = "false",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Boolean convertPDFToImage;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="page-container">
|
<div id="page-container">
|
||||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
<div style="transform-origin: top;"
|
<div style="transform-origin: top;"
|
||||||
id="scale-wrap">
|
id="scale-wrap">
|
||||||
@ -125,11 +125,11 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Survey Modal -->
|
<!-- Survey Modal -->
|
||||||
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel"
|
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -165,11 +165,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Analytics Modal -->
|
<!-- Analytics Modal -->
|
||||||
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel"
|
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel"
|
||||||
aria-hidden="true" th:if="${@analyticsPrompt}">
|
aria-hidden="true" th:if="${@analyticsPrompt}">
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -194,10 +194,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.favorite-icon {
|
.favorite-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 0rem;
|
width: 0rem;
|
||||||
@ -211,17 +211,17 @@
|
|||||||
.toggle-favourites.active {
|
.toggle-favourites.active {
|
||||||
color: gold;
|
color: gold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
/*<![CDATA[*/
|
/*<![CDATA[*/
|
||||||
window.analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
|
window.analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
|
||||||
/*]]>*/
|
/*]]>*/
|
||||||
|
|
||||||
window.showSurvey = /*[[${showSurveyFromDocker}]]*/ true
|
window.showSurvey = /*[[${showSurveyFromDocker}]]*/ true
|
||||||
</script>
|
</script>
|
||||||
<script th:src="@{'/js/pages/home.js'}" th:inline="javascript"></script>
|
<script th:src="@{'/js/pages/home.js'}" th:inline="javascript"></script>
|
||||||
<script>
|
<script>
|
||||||
function applyScale() {
|
function applyScale() {
|
||||||
const baseWidth = 1440;
|
const baseWidth = 1440;
|
||||||
const baseHeight = 1000;
|
const baseHeight = 1000;
|
||||||
@ -234,7 +234,7 @@
|
|||||||
|
|
||||||
window.addEventListener('resize', applyScale);
|
window.addEventListener('resize', applyScale);
|
||||||
window.addEventListener('load', applyScale);
|
window.addEventListener('load', applyScale);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user