mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
PDF Text editor (#4724)
## Summary - add a `PdfJsonConversionService` that serializes PDF text, fonts, and metadata to JSON and rebuilds a PDF from the same structure - expose REST endpoints for `/pdf/json` and `/json/pdf` conversions using the existing convert API infrastructure - define JSON model classes capturing document metadata, font information, and positioned text elements ## Testing - `./gradlew spotlessApply` *(fails: plugin org.springframework.boot:3.5.4 unavailable in build environment)* - `./gradlew build` *(fails: plugin org.springframework.boot:3.5.4 unavailable in build environment)* ------ https://chatgpt.com/codex/tasks/task_b_68f8e98d94ac8328a0e499e541528b6f --------- Co-authored-by: EthanHealy01 <[email protected]>
This commit is contained in:
co-authored by
EthanHealy01
parent
d42065e338
commit
b0397da19e
@@ -306,8 +306,10 @@ jobs:
|
|||||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
|
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
|
||||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw
|
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw
|
||||||
environment:
|
environment:
|
||||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "true"
|
||||||
|
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
|
||||||
|
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
|
||||||
SYSTEM_DEFAULTLOCALE: en-GB
|
SYSTEM_DEFAULTLOCALE: en-GB
|
||||||
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||||
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture"
|
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture"
|
||||||
|
|||||||
+15
@@ -33,6 +33,7 @@ SwaggerDoc.json
|
|||||||
|
|
||||||
# Gradle
|
# Gradle
|
||||||
.gradle
|
.gradle
|
||||||
|
.gradle-home
|
||||||
.lock
|
.lock
|
||||||
|
|
||||||
# External tool builders
|
# External tool builders
|
||||||
@@ -213,3 +214,17 @@ node_modules/
|
|||||||
test_batch.json
|
test_batch.json
|
||||||
*.backup.*.json
|
*.backup.*.json
|
||||||
frontend/public/locales/*/translation.backup*.json
|
frontend/public/locales/*/translation.backup*.json
|
||||||
|
|
||||||
|
# Development/build artifacts
|
||||||
|
.gradle-cache/
|
||||||
|
scripts/pdf-collection/
|
||||||
|
**/tmp/
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Type3 development data
|
||||||
|
docs/type3/signatures/
|
||||||
|
|
||||||
|
|
||||||
|
# Type3 sample PDFs (development only)
|
||||||
|
**/type3/samples/
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ dependencies {
|
|||||||
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
||||||
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
||||||
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
||||||
|
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
|
||||||
|
api "org.apache.pdfbox:preflight:$pdfboxVersion"
|
||||||
api 'com.github.junrar:junrar:7.5.5' // RAR archive support for CBR files
|
api 'com.github.junrar:junrar:7.5.5' // RAR archive support for CBR files
|
||||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||||
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
||||||
|
|||||||
+4
@@ -336,6 +336,8 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Convert", "eml-to-pdf");
|
addEndpointToGroup("Convert", "eml-to-pdf");
|
||||||
addEndpointToGroup("Convert", "cbz-to-pdf");
|
addEndpointToGroup("Convert", "cbz-to-pdf");
|
||||||
addEndpointToGroup("Convert", "pdf-to-cbz");
|
addEndpointToGroup("Convert", "pdf-to-cbz");
|
||||||
|
addEndpointToGroup("Convert", "pdf-to-json");
|
||||||
|
addEndpointToGroup("Convert", "json-to-pdf");
|
||||||
|
|
||||||
// Adding endpoints to "Security" group
|
// Adding endpoints to "Security" group
|
||||||
addEndpointToGroup("Security", "add-password");
|
addEndpointToGroup("Security", "add-password");
|
||||||
@@ -471,6 +473,8 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", "compress-pdf");
|
addEndpointToGroup("Java", "compress-pdf");
|
||||||
addEndpointToGroup("Java", "cbz-to-pdf");
|
addEndpointToGroup("Java", "cbz-to-pdf");
|
||||||
addEndpointToGroup("Java", "pdf-to-cbz");
|
addEndpointToGroup("Java", "pdf-to-cbz");
|
||||||
|
addEndpointToGroup("Java", "pdf-to-json");
|
||||||
|
addEndpointToGroup("Java", "json-to-pdf");
|
||||||
addEndpointToGroup("rar", "pdf-to-cbr");
|
addEndpointToGroup("rar", "pdf-to-cbr");
|
||||||
|
|
||||||
// Javascript
|
// Javascript
|
||||||
+1
-1
@@ -41,7 +41,7 @@ public class CustomPDFDocumentFactory {
|
|||||||
|
|
||||||
// Memory thresholds and limits
|
// Memory thresholds and limits
|
||||||
|
|
||||||
private static final long SMALL_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
|
public static final long SMALL_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
|
||||||
// Files smaller than this threshold are loaded entirely in memory for better performance.
|
// Files smaller than this threshold are loaded entirely in memory for better performance.
|
||||||
// These files use IOUtils.createMemoryOnlyStreamCache() which keeps all document data in RAM.
|
// These files use IOUtils.createMemoryOnlyStreamCache() which keeps all document data in RAM.
|
||||||
// No temp files are created for document data, reducing I/O operations but consuming more
|
// No temp files are created for document data, reducing I/O operations but consuming more
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -37,6 +38,9 @@ public class JobExecutorService {
|
|||||||
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
||||||
private final long effectiveTimeoutMs;
|
private final long effectiveTimeoutMs;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private JobOwnershipService jobOwnershipService;
|
||||||
|
|
||||||
public JobExecutorService(
|
public JobExecutorService(
|
||||||
TaskManager taskManager,
|
TaskManager taskManager,
|
||||||
FileStorage fileStorage,
|
FileStorage fileStorage,
|
||||||
@@ -98,11 +102,17 @@ public class JobExecutorService {
|
|||||||
long customTimeoutMs,
|
long customTimeoutMs,
|
||||||
boolean queueable,
|
boolean queueable,
|
||||||
int resourceWeight) {
|
int resourceWeight) {
|
||||||
String jobId = UUID.randomUUID().toString();
|
// Generate base UUID
|
||||||
|
String baseJobId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
// Store the job ID in the request for potential use by other components
|
// Scope job to authenticated user if security is enabled
|
||||||
|
String scopedJobKey = getScopedJobKey(baseJobId);
|
||||||
|
|
||||||
|
log.debug("Generated jobId: {} (base: {})", scopedJobKey, baseJobId);
|
||||||
|
|
||||||
|
// Store the scoped job ID in the request for potential use by other components
|
||||||
if (request != null) {
|
if (request != null) {
|
||||||
request.setAttribute("jobId", jobId);
|
request.setAttribute("jobId", scopedJobKey);
|
||||||
|
|
||||||
// Also track this job ID in the user's session for authorization purposes
|
// Also track this job ID in the user's session for authorization purposes
|
||||||
// This ensures users can only cancel their own jobs
|
// This ensures users can only cancel their own jobs
|
||||||
@@ -116,11 +126,13 @@ public class JobExecutorService {
|
|||||||
request.getSession().setAttribute("userJobIds", userJobIds);
|
request.getSession().setAttribute("userJobIds", userJobIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
userJobIds.add(jobId);
|
userJobIds.add(scopedJobKey);
|
||||||
log.debug("Added job ID {} to user session", jobId);
|
log.debug("Added scoped job ID {} to user session", scopedJobKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String jobId = scopedJobKey;
|
||||||
|
|
||||||
// Determine which timeout to use
|
// Determine which timeout to use
|
||||||
long timeoutToUse = customTimeoutMs > 0 ? customTimeoutMs : effectiveTimeoutMs;
|
long timeoutToUse = customTimeoutMs > 0 ? customTimeoutMs : effectiveTimeoutMs;
|
||||||
|
|
||||||
@@ -149,17 +161,31 @@ public class JobExecutorService {
|
|||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
// Create a specialized wrapper that updates the TaskManager
|
// Create a specialized wrapper that updates the TaskManager
|
||||||
|
final String capturedJobIdForQueue = jobId;
|
||||||
Supplier<Object> wrappedWork =
|
Supplier<Object> wrappedWork =
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
|
// Set jobId in ThreadLocal context for the queued job
|
||||||
|
stirling.software.common.util.JobContext.setJobId(
|
||||||
|
capturedJobIdForQueue);
|
||||||
|
log.debug(
|
||||||
|
"Set jobId {} in JobContext for queued job execution",
|
||||||
|
capturedJobIdForQueue);
|
||||||
|
|
||||||
Object result = work.get();
|
Object result = work.get();
|
||||||
processJobResult(jobId, result);
|
processJobResult(capturedJobIdForQueue, result);
|
||||||
return result;
|
return result;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(
|
log.error(
|
||||||
"Error executing queued job {}: {}", jobId, e.getMessage(), e);
|
"Error executing queued job {}: {}",
|
||||||
taskManager.setError(jobId, e.getMessage());
|
capturedJobIdForQueue,
|
||||||
|
e.getMessage(),
|
||||||
|
e);
|
||||||
|
taskManager.setError(capturedJobIdForQueue, e.getMessage());
|
||||||
throw e;
|
throw e;
|
||||||
|
} finally {
|
||||||
|
// Clean up ThreadLocal to avoid memory leaks
|
||||||
|
stirling.software.common.util.JobContext.clear();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,21 +197,36 @@ public class JobExecutorService {
|
|||||||
return ResponseEntity.ok().body(new JobResponse<>(true, jobId, null));
|
return ResponseEntity.ok().body(new JobResponse<>(true, jobId, null));
|
||||||
} else if (async) {
|
} else if (async) {
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
|
// Capture the jobId for the async thread
|
||||||
|
final String capturedJobId = jobId;
|
||||||
|
|
||||||
executor.execute(
|
executor.execute(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
log.debug(
|
log.debug(
|
||||||
"Running async job {} with timeout {} ms", jobId, timeoutToUse);
|
"Running async job {} with timeout {} ms",
|
||||||
|
capturedJobId,
|
||||||
|
timeoutToUse);
|
||||||
|
|
||||||
|
// Set jobId in ThreadLocal context for the async thread
|
||||||
|
stirling.software.common.util.JobContext.setJobId(capturedJobId);
|
||||||
|
log.debug(
|
||||||
|
"Set jobId {} in JobContext for async execution",
|
||||||
|
capturedJobId);
|
||||||
|
|
||||||
// Execute with timeout
|
// Execute with timeout
|
||||||
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
|
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
|
||||||
processJobResult(jobId, result);
|
processJobResult(capturedJobId, result);
|
||||||
} catch (TimeoutException te) {
|
} catch (TimeoutException te) {
|
||||||
log.error("Job {} timed out after {} ms", jobId, timeoutToUse);
|
log.error("Job {} timed out after {} ms", jobId, timeoutToUse);
|
||||||
taskManager.setError(jobId, "Job timed out");
|
taskManager.setError(jobId, "Job timed out");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error executing job {}: {}", jobId, e.getMessage(), e);
|
log.error("Error executing job {}: {}", jobId, e.getMessage(), e);
|
||||||
taskManager.setError(jobId, e.getMessage());
|
taskManager.setError(jobId, e.getMessage());
|
||||||
|
} finally {
|
||||||
|
// Clean up ThreadLocal to avoid memory leaks
|
||||||
|
stirling.software.common.util.JobContext.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -194,6 +235,10 @@ public class JobExecutorService {
|
|||||||
try {
|
try {
|
||||||
log.debug("Running sync job with timeout {} ms", timeoutToUse);
|
log.debug("Running sync job with timeout {} ms", timeoutToUse);
|
||||||
|
|
||||||
|
// Make jobId available to downstream components on the worker thread
|
||||||
|
stirling.software.common.util.JobContext.setJobId(jobId);
|
||||||
|
log.debug("Set jobId {} in JobContext for sync execution", jobId);
|
||||||
|
|
||||||
// Execute with timeout
|
// Execute with timeout
|
||||||
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
|
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
|
||||||
|
|
||||||
@@ -213,6 +258,8 @@ public class JobExecutorService {
|
|||||||
// Construct a JSON error response
|
// Construct a JSON error response
|
||||||
return ResponseEntity.internalServerError()
|
return ResponseEntity.internalServerError()
|
||||||
.body(Map.of("error", "Job failed: " + e.getMessage()));
|
.body(Map.of("error", "Job failed: " + e.getMessage()));
|
||||||
|
} finally {
|
||||||
|
stirling.software.common.util.JobContext.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,8 +513,23 @@ public class JobExecutorService {
|
|||||||
throws TimeoutException, Exception {
|
throws TimeoutException, Exception {
|
||||||
// Use the same executor as other async jobs for consistency
|
// Use the same executor as other async jobs for consistency
|
||||||
// This ensures all operations run on the same thread pool
|
// This ensures all operations run on the same thread pool
|
||||||
|
String currentJobId = stirling.software.common.util.JobContext.getJobId();
|
||||||
|
|
||||||
java.util.concurrent.CompletableFuture<T> future =
|
java.util.concurrent.CompletableFuture<T> future =
|
||||||
java.util.concurrent.CompletableFuture.supplyAsync(supplier, executor);
|
java.util.concurrent.CompletableFuture.supplyAsync(
|
||||||
|
() -> {
|
||||||
|
if (currentJobId != null) {
|
||||||
|
stirling.software.common.util.JobContext.setJobId(currentJobId);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return supplier.get();
|
||||||
|
} finally {
|
||||||
|
if (currentJobId != null) {
|
||||||
|
stirling.software.common.util.JobContext.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
executor);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||||
@@ -483,4 +545,18 @@ public class JobExecutorService {
|
|||||||
throw new Exception("Execution was interrupted", e);
|
throw new Exception("Execution was interrupted", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a scoped job key that includes user ownership when security is enabled.
|
||||||
|
*
|
||||||
|
* @param baseJobId the base job identifier
|
||||||
|
* @return scoped job key, or just baseJobId if no ownership service available
|
||||||
|
*/
|
||||||
|
private String getScopedJobKey(String baseJobId) {
|
||||||
|
if (jobOwnershipService != null) {
|
||||||
|
return jobOwnershipService.createScopedJobKey(baseJobId);
|
||||||
|
}
|
||||||
|
// Security disabled, return unsecured job key
|
||||||
|
return baseJobId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service interface for managing job ownership and access control. Implementations can provide
|
||||||
|
* user-scoped job isolation when security is enabled, or no-op behavior when security is disabled.
|
||||||
|
*/
|
||||||
|
public interface JobOwnershipService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current authenticated user's identifier.
|
||||||
|
*
|
||||||
|
* @return Optional containing user identifier, or empty if not authenticated
|
||||||
|
*/
|
||||||
|
Optional<String> getCurrentUserId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a scoped job key that includes user ownership when security is enabled.
|
||||||
|
*
|
||||||
|
* @param jobId the base job identifier
|
||||||
|
* @return scoped job key in format "userId:jobId", or just jobId if no user authenticated
|
||||||
|
*/
|
||||||
|
String createScopedJobKey(String jobId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the current user has access to the given job.
|
||||||
|
*
|
||||||
|
* @param scopedJobKey the scoped job key to validate
|
||||||
|
* @return true if current user owns the job or no authentication is required
|
||||||
|
* @throws SecurityException if current user does not own the job
|
||||||
|
*/
|
||||||
|
boolean validateJobAccess(String scopedJobKey);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the base job ID from a scoped job key.
|
||||||
|
*
|
||||||
|
* @param scopedJobKey the scoped job key
|
||||||
|
* @return the base job ID without user prefix
|
||||||
|
*/
|
||||||
|
String extractJobId(String scopedJobKey);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
/** Thread-local context for passing job ID across async boundaries */
|
||||||
|
public class JobContext {
|
||||||
|
private static final ThreadLocal<String> CURRENT_JOB_ID = new ThreadLocal<>();
|
||||||
|
|
||||||
|
public static void setJobId(String jobId) {
|
||||||
|
CURRENT_JOB_ID.set(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getJobId() {
|
||||||
|
return CURRENT_JOB_ID.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clear() {
|
||||||
|
CURRENT_JOB_ID.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,6 +96,7 @@ public class ProcessExecutor {
|
|||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getSessionLimit()
|
.getSessionLimit()
|
||||||
.getOcrMyPdfSessionLimit();
|
.getOcrMyPdfSessionLimit();
|
||||||
|
case CFF_CONVERTER -> 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
long timeoutMinutes =
|
long timeoutMinutes =
|
||||||
@@ -150,6 +151,7 @@ public class ProcessExecutor {
|
|||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getTimeoutMinutes()
|
.getTimeoutMinutes()
|
||||||
.getOcrMyPdfTimeoutMinutes();
|
.getOcrMyPdfTimeoutMinutes();
|
||||||
|
case CFF_CONVERTER -> 5L;
|
||||||
};
|
};
|
||||||
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
||||||
});
|
});
|
||||||
@@ -302,7 +304,8 @@ public class ProcessExecutor {
|
|||||||
TESSERACT,
|
TESSERACT,
|
||||||
QPDF,
|
QPDF,
|
||||||
GHOSTSCRIPT,
|
GHOSTSCRIPT,
|
||||||
OCR_MY_PDF
|
OCR_MY_PDF,
|
||||||
|
CFF_CONVERTER
|
||||||
}
|
}
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
|
|||||||
@@ -78,6 +78,23 @@ class JobExecutorServiceTest {
|
|||||||
verify(request).setAttribute(eq("jobId"), anyString());
|
verify(request).setAttribute(eq("jobId"), anyString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposeJobIdInJobContextDuringSyncExecution() throws Exception {
|
||||||
|
// Given
|
||||||
|
Supplier<Object> work = stirling.software.common.util.JobContext::getJobId;
|
||||||
|
|
||||||
|
// When
|
||||||
|
ResponseEntity<?> response = jobExecutorService.runJobGeneric(false, work);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertNotNull(response.getBody());
|
||||||
|
|
||||||
|
var requestJobIdCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
verify(request).setAttribute(eq("jobId"), requestJobIdCaptor.capture());
|
||||||
|
assertEquals(requestJobIdCaptor.getValue(), response.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRunAsyncJobSuccessfully() throws Exception {
|
void shouldRunAsyncJobSuccessfully() throws Exception {
|
||||||
// Given
|
// Given
|
||||||
|
|||||||
+3
-3
@@ -196,9 +196,9 @@ public class StampController {
|
|||||||
resourceDir =
|
resourceDir =
|
||||||
switch (alphabet) {
|
switch (alphabet) {
|
||||||
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
|
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
|
||||||
case "japanese" -> "static/fonts/Meiryo.ttf";
|
case "japanese" -> "static/fonts/NotoSansJP-Regular.ttf";
|
||||||
case "korean" -> "static/fonts/malgun.ttf";
|
case "korean" -> "static/fonts/NotoSansKR-Regular.ttf";
|
||||||
case "chinese" -> "static/fonts/SimSun.ttf";
|
case "chinese" -> "static/fonts/NotoSansSC-Regular.ttf";
|
||||||
case "thai" -> "static/fonts/NotoSansThai-Regular.ttf";
|
case "thai" -> "static/fonts/NotoSansThai-Regular.ttf";
|
||||||
case "roman" -> "static/fonts/NotoSans-Regular.ttf";
|
case "roman" -> "static/fonts/NotoSans-Regular.ttf";
|
||||||
default -> "static/fonts/NotoSans-Regular.ttf";
|
default -> "static/fonts/NotoSans-Regular.ttf";
|
||||||
|
|||||||
+3
-3
@@ -171,9 +171,9 @@ public class WatermarkController {
|
|||||||
resourceDir =
|
resourceDir =
|
||||||
switch (alphabet) {
|
switch (alphabet) {
|
||||||
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
|
case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
|
||||||
case "japanese" -> "static/fonts/Meiryo.ttf";
|
case "japanese" -> "static/fonts/NotoSansJP-Regular.ttf";
|
||||||
case "korean" -> "static/fonts/malgun.ttf";
|
case "korean" -> "static/fonts/NotoSansKR-Regular.ttf";
|
||||||
case "chinese" -> "static/fonts/SimSun.ttf";
|
case "chinese" -> "static/fonts/NotoSansSC-Regular.ttf";
|
||||||
case "thai" -> "static/fonts/NotoSansThai-Regular.ttf";
|
case "thai" -> "static/fonts/NotoSansThai-Regular.ttf";
|
||||||
default -> "static/fonts/NotoSans-Regular.ttf";
|
default -> "static/fonts/NotoSans-Regular.ttf";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
@@ -24,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import stirling.software.common.model.job.JobResult;
|
import stirling.software.common.model.job.JobResult;
|
||||||
import stirling.software.common.model.job.ResultFile;
|
import stirling.software.common.model.job.ResultFile;
|
||||||
import stirling.software.common.service.FileStorage;
|
import stirling.software.common.service.FileStorage;
|
||||||
|
import stirling.software.common.service.JobOwnershipService;
|
||||||
import stirling.software.common.service.JobQueue;
|
import stirling.software.common.service.JobQueue;
|
||||||
import stirling.software.common.service.TaskManager;
|
import stirling.software.common.service.TaskManager;
|
||||||
import stirling.software.common.util.RegexPatternUtils;
|
import stirling.software.common.util.RegexPatternUtils;
|
||||||
@@ -41,6 +43,9 @@ public class JobController {
|
|||||||
private final JobQueue jobQueue;
|
private final JobQueue jobQueue;
|
||||||
private final HttpServletRequest request;
|
private final HttpServletRequest request;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private JobOwnershipService jobOwnershipService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the status of a job
|
* Get the status of a job
|
||||||
*
|
*
|
||||||
@@ -50,6 +55,13 @@ public class JobController {
|
|||||||
@GetMapping("/job/{jobId}")
|
@GetMapping("/job/{jobId}")
|
||||||
@Operation(summary = "Get job status")
|
@Operation(summary = "Get job status")
|
||||||
public ResponseEntity<?> getJobStatus(@PathVariable("jobId") String jobId) {
|
public ResponseEntity<?> getJobStatus(@PathVariable("jobId") String jobId) {
|
||||||
|
// Validate job ownership
|
||||||
|
if (!validateJobAccess(jobId)) {
|
||||||
|
log.warn("Unauthorized attempt to access job status: {}", jobId);
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(Map.of("message", "You are not authorized to access this job"));
|
||||||
|
}
|
||||||
|
|
||||||
JobResult result = taskManager.getJobResult(jobId);
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
@@ -79,6 +91,13 @@ public class JobController {
|
|||||||
@GetMapping("/job/{jobId}/result")
|
@GetMapping("/job/{jobId}/result")
|
||||||
@Operation(summary = "Get job result")
|
@Operation(summary = "Get job result")
|
||||||
public ResponseEntity<?> getJobResult(@PathVariable("jobId") String jobId) {
|
public ResponseEntity<?> getJobResult(@PathVariable("jobId") String jobId) {
|
||||||
|
// Validate job ownership
|
||||||
|
if (!validateJobAccess(jobId)) {
|
||||||
|
log.warn("Unauthorized attempt to access job result: {}", jobId);
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(Map.of("message", "You are not authorized to access this job"));
|
||||||
|
}
|
||||||
|
|
||||||
JobResult result = taskManager.getJobResult(jobId);
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
@@ -144,13 +163,8 @@ public class JobController {
|
|||||||
public ResponseEntity<?> cancelJob(@PathVariable("jobId") String jobId) {
|
public ResponseEntity<?> cancelJob(@PathVariable("jobId") String jobId) {
|
||||||
log.debug("Request to cancel job: {}", jobId);
|
log.debug("Request to cancel job: {}", jobId);
|
||||||
|
|
||||||
// Verify that this job belongs to the current user
|
// Validate job ownership
|
||||||
// We can use the current request's session to validate ownership
|
if (!validateJobAccess(jobId)) {
|
||||||
Object sessionJobIds = request.getSession().getAttribute("userJobIds");
|
|
||||||
if (sessionJobIds == null
|
|
||||||
|| !(sessionJobIds instanceof java.util.Set)
|
|
||||||
|| !((java.util.Set<?>) sessionJobIds).contains(jobId)) {
|
|
||||||
// Either no jobs in session or jobId doesn't match user's jobs
|
|
||||||
log.warn("Unauthorized attempt to cancel job: {}", jobId);
|
log.warn("Unauthorized attempt to cancel job: {}", jobId);
|
||||||
return ResponseEntity.status(403)
|
return ResponseEntity.status(403)
|
||||||
.body(Map.of("message", "You are not authorized to cancel this job"));
|
.body(Map.of("message", "You are not authorized to cancel this job"));
|
||||||
@@ -210,6 +224,13 @@ public class JobController {
|
|||||||
@GetMapping("/job/{jobId}/result/files")
|
@GetMapping("/job/{jobId}/result/files")
|
||||||
@Operation(summary = "Get job result files")
|
@Operation(summary = "Get job result files")
|
||||||
public ResponseEntity<?> getJobFiles(@PathVariable("jobId") String jobId) {
|
public ResponseEntity<?> getJobFiles(@PathVariable("jobId") String jobId) {
|
||||||
|
// Validate job ownership
|
||||||
|
if (!validateJobAccess(jobId)) {
|
||||||
|
log.warn("Unauthorized attempt to access job files: {}", jobId);
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.body(Map.of("message", "You are not authorized to access this job"));
|
||||||
|
}
|
||||||
|
|
||||||
JobResult result = taskManager.getJobResult(jobId);
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
@@ -330,4 +351,26 @@ public class JobController {
|
|||||||
return "attachment; filename=\"" + fileName + "\"";
|
return "attachment; filename=\"" + fileName + "\"";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the current user has access to the given job.
|
||||||
|
*
|
||||||
|
* @param jobId the job identifier to validate
|
||||||
|
* @return true if user has access, false otherwise
|
||||||
|
*/
|
||||||
|
private boolean validateJobAccess(String jobId) {
|
||||||
|
// If JobOwnershipService is available (security enabled), use it
|
||||||
|
if (jobOwnershipService != null) {
|
||||||
|
try {
|
||||||
|
return jobOwnershipService.validateJobAccess(jobId);
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
log.warn("Job ownership validation failed for jobId {}: {}", jobId, e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security disabled - allow all access (backwards compatibility)
|
||||||
|
// When security is not enabled, any user can access any job by jobId
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
multipart.enabled=true
|
multipart.enabled=true
|
||||||
logging.level.org.springframework=WARN
|
logging.level.org.springframework=WARN
|
||||||
|
logging.level.org.springframework.security=WARN
|
||||||
logging.level.org.hibernate=WARN
|
logging.level.org.hibernate=WARN
|
||||||
logging.level.org.eclipse.jetty=WARN
|
logging.level.org.eclipse.jetty=WARN
|
||||||
#logging.level.org.springframework.security.oauth2=DEBUG
|
#logging.level.org.springframework.security.oauth2=DEBUG
|
||||||
@@ -7,6 +8,9 @@ logging.level.org.eclipse.jetty=WARN
|
|||||||
#logging.level.org.opensaml=DEBUG
|
#logging.level.org.opensaml=DEBUG
|
||||||
#logging.level.stirling.software.proprietary.security=DEBUG
|
#logging.level.stirling.software.proprietary.security=DEBUG
|
||||||
logging.level.com.zaxxer.hikari=WARN
|
logging.level.com.zaxxer.hikari=WARN
|
||||||
|
logging.level.stirling.software.SPDF.service.PdfJsonConversionService=INFO
|
||||||
|
logging.level.stirling.software.common.service.JobExecutorService=INFO
|
||||||
|
logging.level.stirling.software.common.service.TaskManager=INFO
|
||||||
spring.jpa.open-in-view=false
|
spring.jpa.open-in-view=false
|
||||||
server.forward-headers-strategy=NATIVE
|
server.forward-headers-strategy=NATIVE
|
||||||
server.error.path=/error
|
server.error.path=/error
|
||||||
|
|||||||
@@ -174,6 +174,23 @@ system:
|
|||||||
databaseBackup:
|
databaseBackup:
|
||||||
cron: '0 0 0 * * ?' # Cron expression for automatic database backups "0 0 0 * * ?" daily at midnight
|
cron: '0 0 0 * * ?' # Cron expression for automatic database backups "0 0 0 * * ?" daily at midnight
|
||||||
|
|
||||||
|
stirling:
|
||||||
|
pdf:
|
||||||
|
fallback-font: classpath:/static/fonts/NotoSans-Regular.ttf # Override to point at a custom fallback font
|
||||||
|
json:
|
||||||
|
font-normalization:
|
||||||
|
enabled: false # IMPORTANT: Disable to preserve ToUnicode CMaps for correct font rendering. Ghostscript strips Unicode mappings from CID fonts.
|
||||||
|
cff-converter:
|
||||||
|
enabled: true # Wrap CFF/Type1C fonts as OpenType-CFF for browser compatibility
|
||||||
|
method: python # Converter method: 'python' (fontTools, recommended - wraps as OTF), 'fontforge' (legacy - converts to TTF, may hang on CID fonts)
|
||||||
|
python-command: /opt/venv/bin/python3 # Python interpreter path
|
||||||
|
python-script: /scripts/convert_cff_to_ttf.py # Path to font wrapping script
|
||||||
|
fontforge-command: fontforge # Override if FontForge is installed under a different name/path
|
||||||
|
type3:
|
||||||
|
library:
|
||||||
|
enabled: true # Match common Type3 fonts against the built-in library of converted programs
|
||||||
|
index: classpath:/type3/library/index.json # Override to point at a custom index.json (supports http:, file:, classpath:)
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
appNameNavbar: '' # name displayed on the navigation bar
|
appNameNavbar: '' # name displayed on the navigation bar
|
||||||
logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo)
|
logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,592 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "1867"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "1888"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "2029"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "2069"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "2089"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "2116"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSansMono",
|
||||||
|
"encoding": "2174"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans-Oblique",
|
||||||
|
"encoding": "2192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "2209"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "Cmsy10",
|
||||||
|
"encoding": "2228"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "STIXSizeThreeSym-Regular",
|
||||||
|
"encoding": "2233"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSansDisplay",
|
||||||
|
"encoding": "2239"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "4403"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "4438"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "4519"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "4685"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "4733"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "4782"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "4813"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "4834"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSansMono",
|
||||||
|
"encoding": "4878"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "4906"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "4929"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "4971"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5030"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5052"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5083"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5116"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5143"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5175"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5207"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5243"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "Cmr10",
|
||||||
|
"encoding": "5263"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "Cmex10",
|
||||||
|
"encoding": "5270"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "Cmsy10",
|
||||||
|
"encoding": "5275"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "Cmmi10",
|
||||||
|
"encoding": "5280"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5295"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans-Oblique",
|
||||||
|
"encoding": "5313"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSansDisplay",
|
||||||
|
"encoding": "5319"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5334"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5370"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5399"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5427"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5459"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5486"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5513"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5554"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5601"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5647"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5694"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5732"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5771"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5803"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5861"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5904"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5924"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "5951"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "6084"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "6445"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "7195"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "7409"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "7474"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "7708"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "7747"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "7885"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "9029"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "9617"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "10460"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "11445"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans-Bold",
|
||||||
|
"encoding": "11486"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "11497"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "11543"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "12280"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "12301"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "12350"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "12372"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "12395"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "12416"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "01_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "13324"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "3214"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "3251"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "7190"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "9937"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "10792"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "10852"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "14712"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "18396"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "18719"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans-Bold",
|
||||||
|
"encoding": "18741"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "18778"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "18804"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "20974"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "20993"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "21093"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "21117"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "21141"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "02_Matplotlib.pdf",
|
||||||
|
"fontName": "DejaVuSans",
|
||||||
|
"encoding": "21174"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "03_handout-beginner.pdf",
|
||||||
|
"fontName": "BMQQDV+DejaVuSans",
|
||||||
|
"encoding": "17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "03_handout-beginner.pdf",
|
||||||
|
"fontName": "EVICAO+DejaVuSans-Bold",
|
||||||
|
"encoding": "133"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "03_handout-beginner.pdf",
|
||||||
|
"fontName": "BMQQDV+DejaVuSans",
|
||||||
|
"encoding": "152"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "04_handout-intermediate.pdf",
|
||||||
|
"fontName": "BMQQDV+DejaVuSans",
|
||||||
|
"encoding": "13"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "04_handout-intermediate.pdf",
|
||||||
|
"fontName": "BMQQDV+DejaVuSans",
|
||||||
|
"encoding": "85"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "04_handout-intermediate.pdf",
|
||||||
|
"fontName": "BMQQDV+DejaVuSans",
|
||||||
|
"encoding": "104"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "04_handout-intermediate.pdf",
|
||||||
|
"fontName": "BMQQDV+DejaVuSans",
|
||||||
|
"encoding": "121"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "04_handout-intermediate.pdf",
|
||||||
|
"fontName": "BMQQDV+DejaVuSans",
|
||||||
|
"encoding": "135"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "04_handout-intermediate.pdf",
|
||||||
|
"fontName": "BMQQDV+DejaVuSans",
|
||||||
|
"encoding": "159"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "04_handout-intermediate.pdf",
|
||||||
|
"fontName": "BMQQDV+DejaVuSans",
|
||||||
|
"encoding": "179"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "04_handout-intermediate.pdf",
|
||||||
|
"fontName": "BMQQDV+DejaVuSans",
|
||||||
|
"encoding": "198"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "04_handout-intermediate.pdf",
|
||||||
|
"fontName": "NVMZUP+SourceCodePro-Regular",
|
||||||
|
"encoding": "208"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "04_handout-intermediate.pdf",
|
||||||
|
"fontName": "BMQQDV+DejaVuSans",
|
||||||
|
"encoding": "231"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "04_handout-intermediate.pdf",
|
||||||
|
"fontName": "NVMZUP+SourceCodePro-Regular",
|
||||||
|
"encoding": "241"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "07_matplotlib.pdf",
|
||||||
|
"fontName": "SauceCodePowerline-Bold",
|
||||||
|
"encoding": "22"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "07_matplotlib.pdf",
|
||||||
|
"fontName": "SauceCodePowerline-Regular",
|
||||||
|
"encoding": "47"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "07_matplotlib.pdf",
|
||||||
|
"fontName": "SauceCodePowerline-Regular",
|
||||||
|
"encoding": "65"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "07_matplotlib.pdf",
|
||||||
|
"fontName": "SauceCodePowerline-Bold",
|
||||||
|
"encoding": "110"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "08_matplotlib.pdf",
|
||||||
|
"fontName": "F36",
|
||||||
|
"encoding": "12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "08_matplotlib.pdf",
|
||||||
|
"fontName": "F59",
|
||||||
|
"encoding": "42"
|
||||||
|
}
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user