Merge pull request 'azeem' (#44) from azeem into dev
Reviewed-on: https://gitea.maskantech.in/gitea_admin/pdf/pulls/44
This commit is contained in:
@@ -172,7 +172,12 @@ PYBIND11_MODULE(pdfengine, m) {
|
||||
.def_readonly("content", &pdfengine::PdfPage::AnnotationInfo::content)
|
||||
.def_readonly("timestamp", &pdfengine::PdfPage::AnnotationInfo::timestamp)
|
||||
.def_readonly("page_index", &pdfengine::PdfPage::AnnotationInfo::pageIndex)
|
||||
.def_readonly("paths", &pdfengine::PdfPage::AnnotationInfo::paths);
|
||||
.def_readonly("paths", &pdfengine::PdfPage::AnnotationInfo::paths)
|
||||
.def_readonly("field_name", &pdfengine::PdfPage::AnnotationInfo::fieldName)
|
||||
.def_readonly("field_value", &pdfengine::PdfPage::AnnotationInfo::fieldValue)
|
||||
.def_readonly("field_type", &pdfengine::PdfPage::AnnotationInfo::fieldType)
|
||||
.def_readonly("field_flags", &pdfengine::PdfPage::AnnotationInfo::fieldFlags)
|
||||
.def_readonly("field_options", &pdfengine::PdfPage::AnnotationInfo::fieldOptions);
|
||||
|
||||
py::class_<pdfengine::PdfPage, std::shared_ptr<pdfengine::PdfPage>>(m, "PdfPage")
|
||||
.def_property_readonly("width", &pdfengine::PdfPage::width)
|
||||
|
||||
@@ -143,7 +143,7 @@ public:
|
||||
|
||||
struct AnnotationInfo {
|
||||
std::string id;
|
||||
std::string type; // "highlight", "comment", "ink", "strikeout", "signature"
|
||||
std::string type; // "highlight", "comment", "ink", "strikeout", "signature", "widget"
|
||||
double x = 0.0, y = 0.0, width = 0.0, height = 0.0;
|
||||
std::string color;
|
||||
std::string author;
|
||||
@@ -151,6 +151,13 @@ public:
|
||||
std::string timestamp;
|
||||
int pageIndex = 0;
|
||||
std::vector<std::vector<Point2D>> paths;
|
||||
|
||||
// Form field specific properties
|
||||
std::string fieldName;
|
||||
std::string fieldValue;
|
||||
std::string fieldType;
|
||||
int fieldFlags = 0;
|
||||
std::vector<std::string> fieldOptions;
|
||||
};
|
||||
[[nodiscard]] virtual std::expected<std::vector<AnnotationInfo>, EngineError> extractAnnotations() const = 0;
|
||||
|
||||
|
||||
@@ -643,8 +643,9 @@ PdfDocument::loadFromMemory(const std::vector<uint8_t>& data, const std::string&
|
||||
|
||||
namespace pdfengine::parser {
|
||||
|
||||
PdfiumPage::PdfiumPage(NativePageHandle pageHandle, int pageIndex)
|
||||
: page_(pageHandle), pageIndex_(pageIndex) {}
|
||||
PdfiumPage::PdfiumPage(NativeDocHandle docHandle, NativePageHandle pageHandle, int pageIndex)
|
||||
: doc_(docHandle), page_(pageHandle), pageIndex_(pageIndex) {
|
||||
}
|
||||
|
||||
PdfiumPage::~PdfiumPage() {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
@@ -1184,6 +1185,13 @@ std::expected<std::vector<PdfPage::AnnotationInfo>, EngineError> PdfiumPage::ext
|
||||
if (!page_) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
|
||||
// Initialize form fill environment locally to extract form fields
|
||||
FPDF_FORMFILLINFO formInfo;
|
||||
memset(&formInfo, 0, sizeof(formInfo));
|
||||
formInfo.version = 1;
|
||||
FPDF_FORMHANDLE formHandle = FPDFDOC_InitFormFillEnvironment(doc_, &formInfo);
|
||||
|
||||
std::vector<PdfPage::AnnotationInfo> result;
|
||||
int count = FPDFPage_GetAnnotCount(page_);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
@@ -1217,7 +1225,62 @@ std::expected<std::vector<PdfPage::AnnotationInfo>, EngineError> PdfiumPage::ext
|
||||
} else if (subtype == FPDF_ANNOT_STRIKEOUT) {
|
||||
info.type = "strikeout";
|
||||
} else if (subtype == FPDF_ANNOT_WIDGET) {
|
||||
info.type = "signature";
|
||||
info.type = "widget";
|
||||
|
||||
// Basic field properties using PDFium APIs
|
||||
int fieldType = FPDFAnnot_GetFormFieldType(formHandle, annot);
|
||||
|
||||
// Map FPDF_FORMFIELD_* to our types
|
||||
// 1: PUSHBUTTON, 2: CHECKBOX, 3: RADIOBUTTON, 4: COMBOBOX, 5: LISTBOX, 6: TEXTFIELD, 7: SIGNATURE
|
||||
if (fieldType == 1 || fieldType == 2 || fieldType == 3) {
|
||||
info.fieldType = "Btn";
|
||||
} else if (fieldType == 4 || fieldType == 5) {
|
||||
info.fieldType = "Ch";
|
||||
} else if (fieldType == 6) {
|
||||
info.fieldType = "Tx";
|
||||
} else if (fieldType == 7) {
|
||||
info.fieldType = "Sig";
|
||||
} else {
|
||||
info.fieldType = "Unknown";
|
||||
}
|
||||
|
||||
info.fieldFlags = FPDFAnnot_GetFormFieldFlags(formHandle, annot);
|
||||
|
||||
// Extract Field Name (/T)
|
||||
unsigned long nameLen = FPDFAnnot_GetFormFieldName(formHandle, annot, nullptr, 0);
|
||||
if (nameLen > 2) {
|
||||
std::vector<FPDF_WCHAR> nameBuf(nameLen / 2);
|
||||
FPDFAnnot_GetFormFieldName(formHandle, annot, nameBuf.data(), nameLen);
|
||||
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(nameBuf.data()), nameBuf.size());
|
||||
while (!text.empty() && text.back() == '\0') text.pop_back();
|
||||
info.fieldName = text;
|
||||
}
|
||||
|
||||
// Extract Field Value (/V)
|
||||
unsigned long valLen = FPDFAnnot_GetFormFieldValue(formHandle, annot, nullptr, 0);
|
||||
if (valLen > 2) {
|
||||
std::vector<FPDF_WCHAR> valBuf(valLen / 2);
|
||||
FPDFAnnot_GetFormFieldValue(formHandle, annot, valBuf.data(), valLen);
|
||||
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(valBuf.data()), valBuf.size());
|
||||
while (!text.empty() && text.back() == '\0') text.pop_back();
|
||||
info.fieldValue = text;
|
||||
}
|
||||
|
||||
// Extract Options for Choice fields (/Opt)
|
||||
if (info.fieldType == "Ch") {
|
||||
int optCount = FPDFAnnot_GetOptionCount(formHandle, annot);
|
||||
for (int o = 0; o < optCount; ++o) {
|
||||
unsigned long optLen = FPDFAnnot_GetOptionLabel(formHandle, annot, o, nullptr, 0);
|
||||
if (optLen > 2) {
|
||||
std::vector<FPDF_WCHAR> optBuf(optLen / 2);
|
||||
FPDFAnnot_GetOptionLabel(formHandle, annot, o, optBuf.data(), optLen);
|
||||
std::string optText = utf16le_to_utf8(reinterpret_cast<const char16_t*>(optBuf.data()), optBuf.size());
|
||||
while (!optText.empty() && optText.back() == '\0') optText.pop_back();
|
||||
info.fieldOptions.push_back(optText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
info.type = "unknown";
|
||||
}
|
||||
@@ -1310,6 +1373,9 @@ std::expected<std::vector<PdfPage::AnnotationInfo>, EngineError> PdfiumPage::ext
|
||||
|
||||
FPDFPage_CloseAnnot(annot);
|
||||
}
|
||||
|
||||
FPDFDOC_ExitFormFillEnvironment(formHandle);
|
||||
|
||||
return result;
|
||||
#else
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
@@ -1481,12 +1547,12 @@ std::expected<std::shared_ptr<PdfPage>, EngineError> PdfiumDocument::getPage(int
|
||||
}
|
||||
}
|
||||
|
||||
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
|
||||
if (!page) {
|
||||
FPDF_PAGE pageHandle = FPDF_LoadPage(doc_, pageIndex);
|
||||
if (!pageHandle) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
|
||||
auto pageObj = std::make_shared<PdfiumPage>(page, pageIndex);
|
||||
auto pageObj = std::make_shared<PdfiumPage>(doc_, pageHandle, pageIndex);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pageCacheMutex_);
|
||||
pageCache_[pageIndex] = pageObj;
|
||||
@@ -1694,6 +1760,49 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
|
||||
}
|
||||
|
||||
FPDF_ClosePage(page);
|
||||
} else if (type == "update_field") {
|
||||
if (!op.contains("data") || !op["data"].is_object()) {
|
||||
spdlog::error("update_field operation missing 'data' object");
|
||||
return std::unexpected(EngineError::InvalidFormat);
|
||||
}
|
||||
auto data = op["data"];
|
||||
std::string value;
|
||||
if (data["value"].is_boolean()) {
|
||||
value = data["value"].get<bool>() ? "Yes" : "Off";
|
||||
} else if (data["value"].is_string()) {
|
||||
value = data["value"].get<std::string>();
|
||||
} else {
|
||||
spdlog::error("update_field value must be a string or boolean");
|
||||
return std::unexpected(EngineError::InvalidFormat);
|
||||
}
|
||||
|
||||
std::string id = op.value("id", "");
|
||||
int annotIndex = -1;
|
||||
size_t lastUnderscore = id.find_last_of('_');
|
||||
if (lastUnderscore != std::string::npos) {
|
||||
try {
|
||||
annotIndex = std::stoi(id.substr(lastUnderscore + 1));
|
||||
} catch (...) {
|
||||
annotIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (annotIndex >= 0) {
|
||||
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
|
||||
if (!page) {
|
||||
spdlog::error("Failed to load page index {} for field update", pageIndex);
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page, annotIndex);
|
||||
if (annot) {
|
||||
auto utf16 = utf8_to_utf16le(value);
|
||||
FPDFAnnot_SetStringValue(annot, "V", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
|
||||
|
||||
FPDFPage_GenerateContent(page);
|
||||
FPDFPage_CloseAnnot(annot);
|
||||
}
|
||||
FPDF_ClosePage(page);
|
||||
}
|
||||
} else if (type == "image_overlay") {
|
||||
if (!op.contains("data") || !op["data"].is_object()) {
|
||||
spdlog::error("image_overlay operation missing 'data' object");
|
||||
|
||||
@@ -28,7 +28,7 @@ using NativeTextHandle = void*;
|
||||
|
||||
class PdfiumPage : public PdfPage {
|
||||
public:
|
||||
PdfiumPage(NativePageHandle pageHandle, int pageIndex);
|
||||
PdfiumPage(NativeDocHandle docHandle, NativePageHandle pageHandle, int pageIndex);
|
||||
~PdfiumPage() override;
|
||||
|
||||
PdfiumPage(const PdfiumPage&) = delete;
|
||||
@@ -53,6 +53,7 @@ public:
|
||||
Point2D deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
|
||||
|
||||
private:
|
||||
NativeDocHandle doc_ = nullptr;
|
||||
NativePageHandle page_ = nullptr;
|
||||
mutable NativeTextHandle textPage_ = nullptr;
|
||||
int pageIndex_ = 0;
|
||||
|
||||
@@ -140,6 +140,11 @@ function App() {
|
||||
pageIndex: a.pageIndex,
|
||||
// Ink stroke geometry (top-left page points) so the overlay can redraw it interactively.
|
||||
paths: Array.isArray(a.paths) && a.paths.length > 0 ? a.paths : undefined,
|
||||
fieldName: a.fieldName,
|
||||
fieldValue: a.fieldValue,
|
||||
fieldType: a.fieldType,
|
||||
fieldFlags: a.fieldFlags,
|
||||
fieldOptions: a.fieldOptions,
|
||||
})));
|
||||
if (preservePageRef.current) preservePageRef.current = false;
|
||||
else setCurrentPage(0);
|
||||
@@ -475,6 +480,14 @@ function App() {
|
||||
hasSignature={!!pendingSignature}
|
||||
activeStamp={activeStamp?.label ?? null}
|
||||
annotations={annotations}
|
||||
onFieldChange={(id, value, i) => {
|
||||
applyOps([{
|
||||
id,
|
||||
type: 'update_field',
|
||||
pageIndex: i,
|
||||
data: { value }
|
||||
} as any]);
|
||||
}}
|
||||
searchQuery={searchQuery}
|
||||
searchResults={searchResults}
|
||||
searchCurrentMatch={searchCurrentMatch}
|
||||
|
||||
@@ -23,6 +23,21 @@ export interface RenderParams {
|
||||
rotation: number;
|
||||
}
|
||||
|
||||
export interface Annotation {
|
||||
id: string;
|
||||
pageIndex: number;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fieldName?: string;
|
||||
fieldValue?: string;
|
||||
fieldType?: string;
|
||||
fieldFlags?: number;
|
||||
fieldOptions?: string[];
|
||||
}
|
||||
|
||||
import type { Point } from './coordinateMapping';
|
||||
|
||||
export interface SearchRect {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Rect } from '../lib/coordinateMapping';
|
||||
|
||||
export interface Annotation {
|
||||
id: string;
|
||||
type: 'highlight' | 'signature' | 'strikeout' | 'comment' | 'ink';
|
||||
type: 'highlight' | 'signature' | 'strikeout' | 'comment' | 'ink' | 'widget';
|
||||
bbox: Rect;
|
||||
color?: string;
|
||||
opacity?: number;
|
||||
@@ -13,6 +13,13 @@ export interface Annotation {
|
||||
timestamp?: string;
|
||||
paths?: { x: number; y: number }[][];
|
||||
pageIndex?: number;
|
||||
|
||||
// Form fields
|
||||
fieldName?: string;
|
||||
fieldValue?: string;
|
||||
fieldType?: string;
|
||||
fieldFlags?: number;
|
||||
fieldOptions?: string[];
|
||||
}
|
||||
|
||||
interface AnnotationLayerProps {
|
||||
@@ -22,6 +29,7 @@ interface AnnotationLayerProps {
|
||||
zoom: number;
|
||||
annotations: Annotation[];
|
||||
onAnnotationClick?: (annotation: Annotation) => void;
|
||||
onFieldChange?: (id: string, value: string | boolean, pageIndex: number) => void;
|
||||
}
|
||||
|
||||
export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
|
||||
@@ -31,6 +39,7 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
|
||||
zoom,
|
||||
annotations,
|
||||
onAnnotationClick,
|
||||
onFieldChange,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
@@ -39,7 +48,7 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
|
||||
>
|
||||
{annotations
|
||||
.filter((anno) => anno.pageIndex === undefined || anno.pageIndex === pageIndex)
|
||||
.filter((anno) => ['highlight', 'comment', 'strikeout', 'signature'].includes(anno.type))
|
||||
.filter((anno) => ['highlight', 'comment', 'strikeout', 'signature', 'widget'].includes(anno.type))
|
||||
.map((anno) => {
|
||||
const scaledBbox = {
|
||||
x: anno.bbox.x * zoom,
|
||||
@@ -88,6 +97,52 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{anno.type === 'widget' && (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
{anno.fieldType === 'Tx' && (
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={anno.fieldValue || ''}
|
||||
onBlur={(e) => {
|
||||
if (e.target.value !== (anno.fieldValue || '')) {
|
||||
onFieldChange?.(anno.id, e.target.value, pageIndex);
|
||||
}
|
||||
}}
|
||||
className="w-full h-full bg-blue-50/50 border border-blue-400/50 text-sm px-1 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded-sm"
|
||||
/>
|
||||
)}
|
||||
{anno.fieldType === 'Btn' && (
|
||||
// Very simple checkbox or radio visualization
|
||||
<input
|
||||
type={(anno.fieldFlags ?? 0) & 32768 ? 'radio' : 'checkbox'}
|
||||
defaultChecked={anno.fieldValue === 'Yes' || anno.fieldValue === 'On'}
|
||||
onChange={(e) => onFieldChange?.(anno.id, e.target.checked, pageIndex)}
|
||||
className="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
|
||||
/>
|
||||
)}
|
||||
{anno.fieldType === 'Ch' && (
|
||||
<select
|
||||
defaultValue={anno.fieldValue || ''}
|
||||
onChange={(e) => onFieldChange?.(anno.id, e.target.value, pageIndex)}
|
||||
className="w-full h-full bg-blue-50/50 border border-blue-400/50 text-sm px-1 rounded-sm appearance-none"
|
||||
>
|
||||
{anno.fieldOptions?.map((opt: string, i: number) => (
|
||||
<option key={i} value={opt}>{opt}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{anno.fieldType === 'Sig' && (
|
||||
<div className="w-full h-full border border-dashed border-gray-400 bg-gray-50/50 flex items-center justify-center text-xs text-gray-500">
|
||||
Signature Field
|
||||
</div>
|
||||
)}
|
||||
{(!anno.fieldType || anno.fieldType === 'Unknown') && (
|
||||
<div className="w-full h-full border border-blue-400/50 bg-blue-50/50 flex items-center justify-center text-xs text-blue-500 opacity-50">
|
||||
Form Field
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -33,6 +33,7 @@ interface PDFViewerProps {
|
||||
onPlaceText?: (pageIndex: number, rectPts: Rect, text: string) => void;
|
||||
onPlaceStamp?: (pageIndex: number, pointPts: { x: number; y: number }) => void;
|
||||
onPlaceSignature?: (pageIndex: number, pointPts: { x: number; y: number }) => void;
|
||||
onFieldChange?: (id: string, value: string | boolean, pageIndex: number) => void;
|
||||
}
|
||||
|
||||
interface PageLayout {
|
||||
@@ -69,6 +70,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
onPlaceText,
|
||||
onPlaceStamp,
|
||||
onPlaceSignature,
|
||||
onFieldChange,
|
||||
}, ref) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [scrollPosition, setScrollPosition] = useState({ scrollLeft: 0, scrollTop: 0 });
|
||||
@@ -385,6 +387,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
height={page.height}
|
||||
zoom={zoom}
|
||||
annotations={annotations}
|
||||
onFieldChange={onFieldChange}
|
||||
/>
|
||||
|
||||
{/* Text Selection Dragging Layer */}
|
||||
|
||||
@@ -473,6 +473,13 @@ class AnnotationResponse(BaseModel):
|
||||
# Stroke geometry for ink annotations (top-left page-point space), so the
|
||||
# frontend can redraw them as an interactive overlay rather than a flat image.
|
||||
paths: list[list[dict[str, float]]] = []
|
||||
|
||||
# Form field properties
|
||||
fieldName: str | None = None
|
||||
fieldValue: str | None = None
|
||||
fieldType: str | None = None
|
||||
fieldFlags: int | None = None
|
||||
fieldOptions: list[str] | None = None
|
||||
|
||||
@router.get("/{document_id}/annotations", response_model=list[AnnotationResponse])
|
||||
def get_document_annotations(document_id: str) -> list[AnnotationResponse]:
|
||||
@@ -507,6 +514,11 @@ def get_document_annotations(document_id: str) -> list[AnnotationResponse]:
|
||||
timestamp=getattr(a, "timestamp", None),
|
||||
pageIndex=a.page_index,
|
||||
paths=[[{"x": p.x, "y": p.y} for p in stroke] for stroke in getattr(a, "paths", [])],
|
||||
fieldName=getattr(a, "field_name", None),
|
||||
fieldValue=getattr(a, "field_value", None),
|
||||
fieldType=getattr(a, "field_type", None),
|
||||
fieldFlags=getattr(a, "field_flags", None),
|
||||
fieldOptions=getattr(a, "field_options", None),
|
||||
))
|
||||
except Exception:
|
||||
pass
|
||||
@@ -533,6 +545,31 @@ def export_document(document_id: str):
|
||||
filename = d["filename"]
|
||||
if not filename.endswith(".pdf"):
|
||||
filename += ".pdf"
|
||||
|
||||
# pyrefly: ignore [missing-import]
|
||||
import sys
|
||||
import os
|
||||
roaming_path = os.path.join(os.environ.get("APPDATA", "C:\\Users\\azeem\\AppData\\Roaming"), "Python", "Python312", "site-packages")
|
||||
if roaming_path not in sys.path:
|
||||
sys.path.append(roaming_path)
|
||||
|
||||
import pypdf
|
||||
import io
|
||||
|
||||
# Parse the raw bytes and force NeedAppearances so the viewer
|
||||
# actually renders the filled values.
|
||||
reader = pypdf.PdfReader(io.BytesIO(bytes_data))
|
||||
writer = pypdf.PdfWriter()
|
||||
writer.append(reader)
|
||||
|
||||
acro_form = writer.root_object.get("/AcroForm")
|
||||
if acro_form is not None:
|
||||
acro_form_dict = acro_form.get_object()
|
||||
acro_form_dict[pypdf.generic.NameObject("/NeedAppearances")] = pypdf.generic.BooleanObject(True)
|
||||
|
||||
out_stream = io.BytesIO()
|
||||
writer.write(out_stream)
|
||||
bytes_data = out_stream.getvalue()
|
||||
|
||||
return Response(
|
||||
content=bytes_data,
|
||||
|
||||
@@ -168,6 +168,17 @@ class PageReorderOperation(BaseModel):
|
||||
data: PageReorderData
|
||||
|
||||
|
||||
class UpdateFieldData(BaseModel):
|
||||
value: str | bool
|
||||
|
||||
|
||||
class UpdateFieldOperation(BaseModel):
|
||||
id: str
|
||||
type: Literal["update_field"]
|
||||
pageIndex: int
|
||||
data: UpdateFieldData
|
||||
|
||||
|
||||
EditOperation = Annotated[
|
||||
TextOverlayOperation
|
||||
| RedactionOperation
|
||||
@@ -178,7 +189,8 @@ EditOperation = Annotated[
|
||||
| FreehandOperation
|
||||
| PageRotationOperation
|
||||
| PageDeletionOperation
|
||||
| PageReorderOperation,
|
||||
| PageReorderOperation
|
||||
| UpdateFieldOperation,
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
|
||||
@@ -214,7 +226,8 @@ def apply_edits_impl(document_id: str, request: EditsRequest):
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from PIL import Image
|
||||
# pyrefly: ignore [missing-import]
|
||||
from PIL import Image
|
||||
|
||||
# Remove data URI header if present
|
||||
if "," in img_data_str:
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.colors import black, blue
|
||||
|
||||
def create_form_pdf(filename):
|
||||
c = canvas.Canvas(filename, pagesize=letter)
|
||||
|
||||
c.setFont("Helvetica", 20)
|
||||
c.drawString(50, 750, "Test Form Fields")
|
||||
|
||||
c.setFont("Helvetica", 12)
|
||||
|
||||
# Text Field
|
||||
c.drawString(50, 700, "Name:")
|
||||
form = c.acroForm
|
||||
|
||||
form.textfield(
|
||||
name='fname',
|
||||
tooltip='First Name',
|
||||
x=110, y=695, borderStyle='inset',
|
||||
borderColor=black, fillColor=None,
|
||||
width=200, height=20,
|
||||
textColor=blue, forceBorder=True,
|
||||
value='Jane Doe'
|
||||
)
|
||||
|
||||
# Checkbox
|
||||
c.drawString(50, 650, "Subscribe to newsletter:")
|
||||
form.checkbox(
|
||||
name='cb1',
|
||||
tooltip='Subscribe',
|
||||
x=200, y=645, buttonStyle='check',
|
||||
borderColor=black, fillColor=None,
|
||||
textColor=blue, forceBorder=True,
|
||||
checked=True
|
||||
)
|
||||
|
||||
# Radio Buttons
|
||||
c.drawString(50, 600, "Gender:")
|
||||
c.drawString(110, 600, "Male")
|
||||
c.drawString(170, 600, "Female")
|
||||
|
||||
form.radio(
|
||||
name='radio1',
|
||||
tooltip='Gender',
|
||||
value='Male',
|
||||
selected=False,
|
||||
x=145, y=595, buttonStyle='cross',
|
||||
borderStyle='solid', shape='circle',
|
||||
borderColor=black, fillColor=None,
|
||||
textColor=blue, forceBorder=True
|
||||
)
|
||||
|
||||
form.radio(
|
||||
name='radio1',
|
||||
tooltip='Gender',
|
||||
value='Female',
|
||||
selected=True,
|
||||
x=225, y=595, buttonStyle='cross',
|
||||
borderStyle='solid', shape='circle',
|
||||
borderColor=black, fillColor=None,
|
||||
textColor=blue, forceBorder=True
|
||||
)
|
||||
|
||||
# Choice / Dropdown
|
||||
c.drawString(50, 550, "Country:")
|
||||
form.choice(
|
||||
name='country',
|
||||
tooltip='Country',
|
||||
value='USA',
|
||||
options=[('USA', 'United States'), ('CAN', 'Canada'), ('UK', 'United Kingdom')],
|
||||
x=110, y=545, width=150, height=20,
|
||||
borderStyle='solid', borderColor=black,
|
||||
fillColor=None, textColor=blue, forceBorder=True
|
||||
)
|
||||
|
||||
c.save()
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_form_pdf("test_form.pdf")
|
||||
print("test_form.pdf created successfully.")
|
||||
Binary file not shown.
Reference in New Issue
Block a user