diff --git a/.env.example b/.env.example index f38002f..d552729 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,6 @@ S3_USE_SSL=false S3_ACCESS_KEY=minioadmin S3_SECRET_KEY=minioadmin S3_SESSION_TTL_SECONDS=3600 -CONVERSION_IMAGE_DPI=72 CONVERSION_PPTX_TO_PDF_TIMEOUT_SECONDS=180 CONVERSION_PDF_TO_IMAGES_TIMEOUT_SECONDS=1800 CONVERSION_PPTX_TO_PDF_BASE_TIMEOUT_SECONDS=45 diff --git a/Makefile b/Makefile index deee93d..3165fef 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,6 @@ run-server: export S3_ACCESS_KEY="$${S3_ACCESS_KEY:-minioadmin}"; \ export S3_SECRET_KEY="$${S3_SECRET_KEY:-minioadmin}"; \ export S3_SESSION_TTL_SECONDS="$${S3_SESSION_TTL_SECONDS:-3600}"; \ - export CONVERSION_IMAGE_DPI="$${CONVERSION_IMAGE_DPI:-72}"; \ export CONVERSION_PPTX_TO_PDF_TIMEOUT_SECONDS="$${CONVERSION_PPTX_TO_PDF_TIMEOUT_SECONDS:-180}"; \ export CONVERSION_PDF_TO_IMAGES_TIMEOUT_SECONDS="$${CONVERSION_PDF_TO_IMAGES_TIMEOUT_SECONDS:-1800}"; \ export CONVERSION_PPTX_TO_PDF_BASE_TIMEOUT_SECONDS="$${CONVERSION_PPTX_TO_PDF_BASE_TIMEOUT_SECONDS:-45}"; \ diff --git a/README.md b/README.md index 7f58913..2915db6 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ make run-server - defaults S3 endpoint to `localhost:8333` for host-based development - auto-normalizes `seaweedfs:8333` to `localhost:8333` for host runs - supports optional `UVICORN_HOST` and `UVICORN_PORT` overrides -- exposes conversion tuning vars (`CONVERSION_IMAGE_DPI`, `CONVERSION_PPTX_TO_PDF_TIMEOUT_SECONDS`, `CONVERSION_PDF_TO_IMAGES_TIMEOUT_SECONDS`) +- exposes conversion timeout tuning vars (`CONVERSION_PPTX_TO_PDF_TIMEOUT_SECONDS`, `CONVERSION_PDF_TO_IMAGES_TIMEOUT_SECONDS`) Server endpoint base URL: @@ -107,7 +107,7 @@ Create a conversion request: ```bash curl \ --header "Content-Type: application/json" \ - --data '{"sourceFilename":"example.pptx"}' \ + --data '{"sourceFilename":"example.pptx","resolution":"CONVERSION_RESOLUTION_FHD"}' \ http://localhost:8080/officeconvertapi.v1.ConversionService/CreateConversion ``` @@ -139,6 +139,8 @@ Use `.env.example` as your baseline env configuration. If conversion fails on larger decks, tune these environment variables: -- `CONVERSION_IMAGE_DPI` (default `72`): lower values reduce image generation time. +- `CreateConversionRequest.resolution` controls output dimensions via presets: `SD`, `HD`, `FHD`, `QHD`, `UHD`. +- Omitting `resolution` (or sending `CONVERSION_RESOLUTION_UNSPECIFIED`) defaults to `FHD`. +- Rasterization DPI is inferred automatically from source slide size and selected output dimensions. - `CONVERSION_PPTX_TO_PDF_TIMEOUT_SECONDS` (default `180`): timeout for LibreOffice export. - `CONVERSION_PDF_TO_IMAGES_TIMEOUT_SECONDS` (default `1800`): timeout for Poppler rasterization. diff --git a/clients/go/officeconvertclient/client.go b/clients/go/officeconvertclient/client.go index 06db30e..dfd01cd 100644 --- a/clients/go/officeconvertclient/client.go +++ b/clients/go/officeconvertclient/client.go @@ -46,9 +46,11 @@ func (c *Client) SetPollInterval(interval time.Duration) { func (c *Client) CreateConversion( ctx context.Context, sourceFilename string, + resolution officeconvertapiv1.ConversionResolution, ) (*officeconvertapiv1.CreateConversionResponse, error) { req := connect.NewRequest(&officeconvertapiv1.CreateConversionRequest{ SourceFilename: sourceFilename, + Resolution: resolution, }) res, err := c.rpc.CreateConversion(ctx, req) if err != nil { diff --git a/clients/go/officeconvertclient/orchestrate.go b/clients/go/officeconvertclient/orchestrate.go index b605114..4d7c729 100644 --- a/clients/go/officeconvertclient/orchestrate.go +++ b/clients/go/officeconvertclient/orchestrate.go @@ -16,7 +16,11 @@ type ConversionResult struct { // ConvertPPTXFile runs the full create-upload-start-wait-fetch flow. func (c *Client) ConvertPPTXFile(ctx context.Context, localPPTXPath string) (*ConversionResult, error) { - createRes, err := c.CreateConversion(ctx, filepath.Base(localPPTXPath)) + createRes, err := c.CreateConversion( + ctx, + filepath.Base(localPPTXPath), + officeconvertapiv1.ConversionResolution_CONVERSION_RESOLUTION_UNSPECIFIED, + ) if err != nil { return nil, fmt.Errorf("create conversion: %w", err) } diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 190e980..094a230 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -27,7 +27,6 @@ services: S3_ACCESS_KEY: ${S3_ACCESS_KEY:-minioadmin} S3_SECRET_KEY: ${S3_SECRET_KEY:-minioadmin} S3_SESSION_TTL_SECONDS: ${S3_SESSION_TTL_SECONDS:-3600} - CONVERSION_IMAGE_DPI: ${CONVERSION_IMAGE_DPI:-72} CONVERSION_PPTX_TO_PDF_TIMEOUT_SECONDS: ${CONVERSION_PPTX_TO_PDF_TIMEOUT_SECONDS:-180} CONVERSION_PDF_TO_IMAGES_TIMEOUT_SECONDS: ${CONVERSION_PDF_TO_IMAGES_TIMEOUT_SECONDS:-1800} CONVERSION_PPTX_TO_PDF_BASE_TIMEOUT_SECONDS: ${CONVERSION_PPTX_TO_PDF_BASE_TIMEOUT_SECONDS:-45} diff --git a/gen/go/officeconvertapi/v1/conversion.pb.go b/gen/go/officeconvertapi/v1/conversion.pb.go index d8fe3a7..0c22e05 100644 --- a/gen/go/officeconvertapi/v1/conversion.pb.go +++ b/gen/go/officeconvertapi/v1/conversion.pb.go @@ -137,6 +137,65 @@ func (ConversionPhase) EnumDescriptor() ([]byte, []int) { return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{1} } +// ConversionResolution represents preset output quality targets. +type ConversionResolution int32 + +const ( + ConversionResolution_CONVERSION_RESOLUTION_UNSPECIFIED ConversionResolution = 0 + ConversionResolution_CONVERSION_RESOLUTION_SD ConversionResolution = 1 + ConversionResolution_CONVERSION_RESOLUTION_HD ConversionResolution = 2 + ConversionResolution_CONVERSION_RESOLUTION_FHD ConversionResolution = 3 + ConversionResolution_CONVERSION_RESOLUTION_QHD ConversionResolution = 4 + ConversionResolution_CONVERSION_RESOLUTION_UHD ConversionResolution = 5 +) + +// Enum value maps for ConversionResolution. +var ( + ConversionResolution_name = map[int32]string{ + 0: "CONVERSION_RESOLUTION_UNSPECIFIED", + 1: "CONVERSION_RESOLUTION_SD", + 2: "CONVERSION_RESOLUTION_HD", + 3: "CONVERSION_RESOLUTION_FHD", + 4: "CONVERSION_RESOLUTION_QHD", + 5: "CONVERSION_RESOLUTION_UHD", + } + ConversionResolution_value = map[string]int32{ + "CONVERSION_RESOLUTION_UNSPECIFIED": 0, + "CONVERSION_RESOLUTION_SD": 1, + "CONVERSION_RESOLUTION_HD": 2, + "CONVERSION_RESOLUTION_FHD": 3, + "CONVERSION_RESOLUTION_QHD": 4, + "CONVERSION_RESOLUTION_UHD": 5, + } +) + +func (x ConversionResolution) Enum() *ConversionResolution { + p := new(ConversionResolution) + *p = x + return p +} + +func (x ConversionResolution) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConversionResolution) Descriptor() protoreflect.EnumDescriptor { + return file_officeconvertapi_v1_conversion_proto_enumTypes[2].Descriptor() +} + +func (ConversionResolution) Type() protoreflect.EnumType { + return &file_officeconvertapi_v1_conversion_proto_enumTypes[2] +} + +func (x ConversionResolution) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConversionResolution.Descriptor instead. +func (ConversionResolution) EnumDescriptor() ([]byte, []int) { + return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{2} +} + // Slide contains extracted notes and the rendered image URL for one slide. type Slide struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -205,6 +264,8 @@ type SlideDeck struct { SourceFilename string `protobuf:"bytes,2,opt,name=source_filename,json=sourceFilename,proto3" json:"source_filename,omitempty"` Slides []*Slide `protobuf:"bytes,3,rep,name=slides,proto3" json:"slides,omitempty"` CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + Width int32 `protobuf:"varint,5,opt,name=width,proto3" json:"width,omitempty"` + Height int32 `protobuf:"varint,6,opt,name=height,proto3" json:"height,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -267,10 +328,25 @@ func (x *SlideDeck) GetCreatedAt() *timestamppb.Timestamp { return nil } +func (x *SlideDeck) GetWidth() int32 { + if x != nil { + return x.Width + } + return 0 +} + +func (x *SlideDeck) GetHeight() int32 { + if x != nil { + return x.Height + } + return 0 +} + // CreateConversionRequest starts a conversion session. type CreateConversionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` SourceFilename string `protobuf:"bytes,1,opt,name=source_filename,json=sourceFilename,proto3" json:"source_filename,omitempty"` + Resolution ConversionResolution `protobuf:"varint,2,opt,name=resolution,proto3,enum=officeconvertapi.v1.ConversionResolution" json:"resolution,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -312,6 +388,13 @@ func (x *CreateConversionRequest) GetSourceFilename() string { return "" } +func (x *CreateConversionRequest) GetResolution() ConversionResolution { + if x != nil { + return x.Resolution + } + return ConversionResolution_CONVERSION_RESOLUTION_UNSPECIFIED +} + // CreateConversionResponse returns upload details for the session. type CreateConversionResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -822,15 +905,20 @@ const file_officeconvertapi_v1_conversion_proto_rawDesc = "" + "\x05index\x18\x01 \x01(\x05R\x05index\x12\x1f\n" + "\vnotes_plain\x18\x02 \x01(\tR\n" + "notesPlain\x12\x1b\n" + - "\timage_url\x18\x03 \x01(\tR\bimageUrl\"\xc8\x01\n" + + "\timage_url\x18\x03 \x01(\tR\bimageUrl\"\xf6\x01\n" + "\tSlideDeck\x12#\n" + "\rconversion_id\x18\x01 \x01(\tR\fconversionId\x12'\n" + "\x0fsource_filename\x18\x02 \x01(\tR\x0esourceFilename\x122\n" + "\x06slides\x18\x03 \x03(\v2\x1a.officeconvertapi.v1.SlideR\x06slides\x129\n" + "\n" + - "created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"B\n" + + "created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x14\n" + + "\x05width\x18\x05 \x01(\x05R\x05width\x12\x16\n" + + "\x06height\x18\x06 \x01(\x05R\x06height\"\x8d\x01\n" + "\x17CreateConversionRequest\x12'\n" + - "\x0fsource_filename\x18\x01 \x01(\tR\x0esourceFilename\"\xea\x01\n" + + "\x0fsource_filename\x18\x01 \x01(\tR\x0esourceFilename\x12I\n" + + "\n" + + "resolution\x18\x02 \x01(\x0e2).officeconvertapi.v1.ConversionResolutionR\n" + + "resolution\"\xea\x01\n" + "\x18CreateConversionResponse\x12#\n" + "\rconversion_id\x18\x01 \x01(\tR\fconversionId\x12#\n" + "\rupload_bucket\x18\x02 \x01(\tR\fuploadBucket\x12*\n" + @@ -877,7 +965,14 @@ const file_officeconvertapi_v1_conversion_proto_rawDesc = "" + "!CONVERSION_PHASE_EXTRACTING_NOTES\x10\x02\x12 \n" + "\x1cCONVERSION_PHASE_PPTX_TO_PDF\x10\x03\x12\"\n" + "\x1eCONVERSION_PHASE_PDF_TO_IMAGES\x10\x04\x12&\n" + - "\"CONVERSION_PHASE_UPLOADING_RESULTS\x10\x052\xcc\x04\n" + + "\"CONVERSION_PHASE_UPLOADING_RESULTS\x10\x05*\xd6\x01\n" + + "\x14ConversionResolution\x12%\n" + + "!CONVERSION_RESOLUTION_UNSPECIFIED\x10\x00\x12\x1c\n" + + "\x18CONVERSION_RESOLUTION_SD\x10\x01\x12\x1c\n" + + "\x18CONVERSION_RESOLUTION_HD\x10\x02\x12\x1d\n" + + "\x19CONVERSION_RESOLUTION_FHD\x10\x03\x12\x1d\n" + + "\x19CONVERSION_RESOLUTION_QHD\x10\x04\x12\x1d\n" + + "\x19CONVERSION_RESOLUTION_UHD\x10\x052\xcc\x04\n" + "\x11ConversionService\x12q\n" + "\x10CreateConversion\x12,.officeconvertapi.v1.CreateConversionRequest\x1a-.officeconvertapi.v1.CreateConversionResponse\"\x00\x12n\n" + "\x0fStartConversion\x12+.officeconvertapi.v1.StartConversionRequest\x1a,.officeconvertapi.v1.StartConversionResponse\"\x00\x12z\n" + @@ -897,49 +992,51 @@ func file_officeconvertapi_v1_conversion_proto_rawDescGZIP() []byte { return file_officeconvertapi_v1_conversion_proto_rawDescData } -var file_officeconvertapi_v1_conversion_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_officeconvertapi_v1_conversion_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_officeconvertapi_v1_conversion_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_officeconvertapi_v1_conversion_proto_goTypes = []any{ (ConversionStatus)(0), // 0: officeconvertapi.v1.ConversionStatus (ConversionPhase)(0), // 1: officeconvertapi.v1.ConversionPhase - (*Slide)(nil), // 2: officeconvertapi.v1.Slide - (*SlideDeck)(nil), // 3: officeconvertapi.v1.SlideDeck - (*CreateConversionRequest)(nil), // 4: officeconvertapi.v1.CreateConversionRequest - (*CreateConversionResponse)(nil), // 5: officeconvertapi.v1.CreateConversionResponse - (*StartConversionRequest)(nil), // 6: officeconvertapi.v1.StartConversionRequest - (*StartConversionResponse)(nil), // 7: officeconvertapi.v1.StartConversionResponse - (*GetConversionStatusRequest)(nil), // 8: officeconvertapi.v1.GetConversionStatusRequest - (*GetConversionStatusResponse)(nil), // 9: officeconvertapi.v1.GetConversionStatusResponse - (*GetSlideDeckRequest)(nil), // 10: officeconvertapi.v1.GetSlideDeckRequest - (*GetSlideDeckResponse)(nil), // 11: officeconvertapi.v1.GetSlideDeckResponse - (*DeleteConversionRequest)(nil), // 12: officeconvertapi.v1.DeleteConversionRequest - (*DeleteConversionResponse)(nil), // 13: officeconvertapi.v1.DeleteConversionResponse - (*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp + (ConversionResolution)(0), // 2: officeconvertapi.v1.ConversionResolution + (*Slide)(nil), // 3: officeconvertapi.v1.Slide + (*SlideDeck)(nil), // 4: officeconvertapi.v1.SlideDeck + (*CreateConversionRequest)(nil), // 5: officeconvertapi.v1.CreateConversionRequest + (*CreateConversionResponse)(nil), // 6: officeconvertapi.v1.CreateConversionResponse + (*StartConversionRequest)(nil), // 7: officeconvertapi.v1.StartConversionRequest + (*StartConversionResponse)(nil), // 8: officeconvertapi.v1.StartConversionResponse + (*GetConversionStatusRequest)(nil), // 9: officeconvertapi.v1.GetConversionStatusRequest + (*GetConversionStatusResponse)(nil), // 10: officeconvertapi.v1.GetConversionStatusResponse + (*GetSlideDeckRequest)(nil), // 11: officeconvertapi.v1.GetSlideDeckRequest + (*GetSlideDeckResponse)(nil), // 12: officeconvertapi.v1.GetSlideDeckResponse + (*DeleteConversionRequest)(nil), // 13: officeconvertapi.v1.DeleteConversionRequest + (*DeleteConversionResponse)(nil), // 14: officeconvertapi.v1.DeleteConversionResponse + (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp } var file_officeconvertapi_v1_conversion_proto_depIdxs = []int32{ - 2, // 0: officeconvertapi.v1.SlideDeck.slides:type_name -> officeconvertapi.v1.Slide - 14, // 1: officeconvertapi.v1.SlideDeck.created_at:type_name -> google.protobuf.Timestamp - 14, // 2: officeconvertapi.v1.CreateConversionResponse.expires_at:type_name -> google.protobuf.Timestamp - 0, // 3: officeconvertapi.v1.StartConversionResponse.status:type_name -> officeconvertapi.v1.ConversionStatus - 0, // 4: officeconvertapi.v1.GetConversionStatusResponse.status:type_name -> officeconvertapi.v1.ConversionStatus - 14, // 5: officeconvertapi.v1.GetConversionStatusResponse.updated_at:type_name -> google.protobuf.Timestamp - 1, // 6: officeconvertapi.v1.GetConversionStatusResponse.phase:type_name -> officeconvertapi.v1.ConversionPhase - 3, // 7: officeconvertapi.v1.GetSlideDeckResponse.slide_deck:type_name -> officeconvertapi.v1.SlideDeck - 4, // 8: officeconvertapi.v1.ConversionService.CreateConversion:input_type -> officeconvertapi.v1.CreateConversionRequest - 6, // 9: officeconvertapi.v1.ConversionService.StartConversion:input_type -> officeconvertapi.v1.StartConversionRequest - 8, // 10: officeconvertapi.v1.ConversionService.GetConversionStatus:input_type -> officeconvertapi.v1.GetConversionStatusRequest - 10, // 11: officeconvertapi.v1.ConversionService.GetSlideDeck:input_type -> officeconvertapi.v1.GetSlideDeckRequest - 12, // 12: officeconvertapi.v1.ConversionService.DeleteConversion:input_type -> officeconvertapi.v1.DeleteConversionRequest - 5, // 13: officeconvertapi.v1.ConversionService.CreateConversion:output_type -> officeconvertapi.v1.CreateConversionResponse - 7, // 14: officeconvertapi.v1.ConversionService.StartConversion:output_type -> officeconvertapi.v1.StartConversionResponse - 9, // 15: officeconvertapi.v1.ConversionService.GetConversionStatus:output_type -> officeconvertapi.v1.GetConversionStatusResponse - 11, // 16: officeconvertapi.v1.ConversionService.GetSlideDeck:output_type -> officeconvertapi.v1.GetSlideDeckResponse - 13, // 17: officeconvertapi.v1.ConversionService.DeleteConversion:output_type -> officeconvertapi.v1.DeleteConversionResponse - 13, // [13:18] is the sub-list for method output_type - 8, // [8:13] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 3, // 0: officeconvertapi.v1.SlideDeck.slides:type_name -> officeconvertapi.v1.Slide + 15, // 1: officeconvertapi.v1.SlideDeck.created_at:type_name -> google.protobuf.Timestamp + 2, // 2: officeconvertapi.v1.CreateConversionRequest.resolution:type_name -> officeconvertapi.v1.ConversionResolution + 15, // 3: officeconvertapi.v1.CreateConversionResponse.expires_at:type_name -> google.protobuf.Timestamp + 0, // 4: officeconvertapi.v1.StartConversionResponse.status:type_name -> officeconvertapi.v1.ConversionStatus + 0, // 5: officeconvertapi.v1.GetConversionStatusResponse.status:type_name -> officeconvertapi.v1.ConversionStatus + 15, // 6: officeconvertapi.v1.GetConversionStatusResponse.updated_at:type_name -> google.protobuf.Timestamp + 1, // 7: officeconvertapi.v1.GetConversionStatusResponse.phase:type_name -> officeconvertapi.v1.ConversionPhase + 4, // 8: officeconvertapi.v1.GetSlideDeckResponse.slide_deck:type_name -> officeconvertapi.v1.SlideDeck + 5, // 9: officeconvertapi.v1.ConversionService.CreateConversion:input_type -> officeconvertapi.v1.CreateConversionRequest + 7, // 10: officeconvertapi.v1.ConversionService.StartConversion:input_type -> officeconvertapi.v1.StartConversionRequest + 9, // 11: officeconvertapi.v1.ConversionService.GetConversionStatus:input_type -> officeconvertapi.v1.GetConversionStatusRequest + 11, // 12: officeconvertapi.v1.ConversionService.GetSlideDeck:input_type -> officeconvertapi.v1.GetSlideDeckRequest + 13, // 13: officeconvertapi.v1.ConversionService.DeleteConversion:input_type -> officeconvertapi.v1.DeleteConversionRequest + 6, // 14: officeconvertapi.v1.ConversionService.CreateConversion:output_type -> officeconvertapi.v1.CreateConversionResponse + 8, // 15: officeconvertapi.v1.ConversionService.StartConversion:output_type -> officeconvertapi.v1.StartConversionResponse + 10, // 16: officeconvertapi.v1.ConversionService.GetConversionStatus:output_type -> officeconvertapi.v1.GetConversionStatusResponse + 12, // 17: officeconvertapi.v1.ConversionService.GetSlideDeck:output_type -> officeconvertapi.v1.GetSlideDeckResponse + 14, // 18: officeconvertapi.v1.ConversionService.DeleteConversion:output_type -> officeconvertapi.v1.DeleteConversionResponse + 14, // [14:19] is the sub-list for method output_type + 9, // [9:14] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_officeconvertapi_v1_conversion_proto_init() } @@ -952,7 +1049,7 @@ func file_officeconvertapi_v1_conversion_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_officeconvertapi_v1_conversion_proto_rawDesc), len(file_officeconvertapi_v1_conversion_proto_rawDesc)), - NumEnums: 2, + NumEnums: 3, NumMessages: 12, NumExtensions: 0, NumServices: 1, diff --git a/gen/python/officeconvertapi/v1/conversion_pb2.py b/gen/python/officeconvertapi/v1/conversion_pb2.py index c7a25d8..163db56 100644 --- a/gen/python/officeconvertapi/v1/conversion_pb2.py +++ b/gen/python/officeconvertapi/v1/conversion_pb2.py @@ -25,7 +25,7 @@ _sym_db = _symbol_database.Default() from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$officeconvertapi/v1/conversion.proto\x12\x13officeconvertapi.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"[\n\x05Slide\x12\x14\n\x05index\x18\x01 \x01(\x05R\x05index\x12\x1f\n\x0bnotes_plain\x18\x02 \x01(\tR\nnotesPlain\x12\x1b\n\timage_url\x18\x03 \x01(\tR\x08imageUrl\"\xc8\x01\n\tSlideDeck\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12\'\n\x0fsource_filename\x18\x02 \x01(\tR\x0esourceFilename\x12\x32\n\x06slides\x18\x03 \x03(\x0b\x32\x1a.officeconvertapi.v1.SlideR\x06slides\x12\x39\n\ncreated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\"B\n\x17\x43reateConversionRequest\x12\'\n\x0fsource_filename\x18\x01 \x01(\tR\x0esourceFilename\"\xea\x01\n\x18\x43reateConversionResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12#\n\rupload_bucket\x18\x02 \x01(\tR\x0cuploadBucket\x12*\n\x11upload_object_key\x18\x03 \x01(\tR\x0fuploadObjectKey\x12\x1d\n\nupload_url\x18\x04 \x01(\tR\tuploadUrl\x12\x39\n\nexpires_at\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\texpiresAt\"=\n\x16StartConversionRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"}\n\x17StartConversionResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12=\n\x06status\x18\x02 \x01(\x0e\x32%.officeconvertapi.v1.ConversionStatusR\x06status\"A\n\x1aGetConversionStatusRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"\xeb\x02\n\x1bGetConversionStatusResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12=\n\x06status\x18\x02 \x01(\x0e\x32%.officeconvertapi.v1.ConversionStatusR\x06status\x12#\n\rerror_message\x18\x03 \x01(\tR\x0c\x65rrorMessage\x12\x39\n\nupdated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tupdatedAt\x12:\n\x05phase\x18\x05 \x01(\x0e\x32$.officeconvertapi.v1.ConversionPhaseR\x05phase\x12)\n\x10\x63urrent_progress\x18\x06 \x01(\x05R\x0f\x63urrentProgress\x12!\n\x0cmax_progress\x18\x07 \x01(\x05R\x0bmaxProgress\":\n\x13GetSlideDeckRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"U\n\x14GetSlideDeckResponse\x12=\n\nslide_deck\x18\x01 \x01(\x0b\x32\x1e.officeconvertapi.v1.SlideDeckR\tslideDeck\">\n\x17\x44\x65leteConversionRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"Y\n\x18\x44\x65leteConversionResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12\x18\n\x07\x64\x65leted\x18\x02 \x01(\x08R\x07\x64\x65leted*\xb2\x01\n\x10\x43onversionStatus\x12!\n\x1d\x43ONVERSION_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x43ONVERSION_STATUS_PENDING\x10\x01\x12\x1d\n\x19\x43ONVERSION_STATUS_RUNNING\x10\x02\x12\x1f\n\x1b\x43ONVERSION_STATUS_SUCCEEDED\x10\x03\x12\x1c\n\x18\x43ONVERSION_STATUS_FAILED\x10\x04*\xe7\x01\n\x0f\x43onversionPhase\x12 \n\x1c\x43ONVERSION_PHASE_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x43ONVERSION_PHASE_INACTIVE\x10\x01\x12%\n!CONVERSION_PHASE_EXTRACTING_NOTES\x10\x02\x12 \n\x1c\x43ONVERSION_PHASE_PPTX_TO_PDF\x10\x03\x12\"\n\x1e\x43ONVERSION_PHASE_PDF_TO_IMAGES\x10\x04\x12&\n\"CONVERSION_PHASE_UPLOADING_RESULTS\x10\x05\x32\xcc\x04\n\x11\x43onversionService\x12q\n\x10\x43reateConversion\x12,.officeconvertapi.v1.CreateConversionRequest\x1a-.officeconvertapi.v1.CreateConversionResponse\"\x00\x12n\n\x0fStartConversion\x12+.officeconvertapi.v1.StartConversionRequest\x1a,.officeconvertapi.v1.StartConversionResponse\"\x00\x12z\n\x13GetConversionStatus\x12/.officeconvertapi.v1.GetConversionStatusRequest\x1a\x30.officeconvertapi.v1.GetConversionStatusResponse\"\x00\x12\x65\n\x0cGetSlideDeck\x12(.officeconvertapi.v1.GetSlideDeckRequest\x1a).officeconvertapi.v1.GetSlideDeckResponse\"\x00\x12q\n\x10\x44\x65leteConversion\x12,.officeconvertapi.v1.DeleteConversionRequest\x1a-.officeconvertapi.v1.DeleteConversionResponse\"\x00\x42LZJgithub.com/end/officeconvert/gen/go/officeconvertapi/v1;officeconvertapiv1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$officeconvertapi/v1/conversion.proto\x12\x13officeconvertapi.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"[\n\x05Slide\x12\x14\n\x05index\x18\x01 \x01(\x05R\x05index\x12\x1f\n\x0bnotes_plain\x18\x02 \x01(\tR\nnotesPlain\x12\x1b\n\timage_url\x18\x03 \x01(\tR\x08imageUrl\"\xf6\x01\n\tSlideDeck\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12\'\n\x0fsource_filename\x18\x02 \x01(\tR\x0esourceFilename\x12\x32\n\x06slides\x18\x03 \x03(\x0b\x32\x1a.officeconvertapi.v1.SlideR\x06slides\x12\x39\n\ncreated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x14\n\x05width\x18\x05 \x01(\x05R\x05width\x12\x16\n\x06height\x18\x06 \x01(\x05R\x06height\"\x8d\x01\n\x17\x43reateConversionRequest\x12\'\n\x0fsource_filename\x18\x01 \x01(\tR\x0esourceFilename\x12I\n\nresolution\x18\x02 \x01(\x0e\x32).officeconvertapi.v1.ConversionResolutionR\nresolution\"\xea\x01\n\x18\x43reateConversionResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12#\n\rupload_bucket\x18\x02 \x01(\tR\x0cuploadBucket\x12*\n\x11upload_object_key\x18\x03 \x01(\tR\x0fuploadObjectKey\x12\x1d\n\nupload_url\x18\x04 \x01(\tR\tuploadUrl\x12\x39\n\nexpires_at\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\texpiresAt\"=\n\x16StartConversionRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"}\n\x17StartConversionResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12=\n\x06status\x18\x02 \x01(\x0e\x32%.officeconvertapi.v1.ConversionStatusR\x06status\"A\n\x1aGetConversionStatusRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"\xeb\x02\n\x1bGetConversionStatusResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12=\n\x06status\x18\x02 \x01(\x0e\x32%.officeconvertapi.v1.ConversionStatusR\x06status\x12#\n\rerror_message\x18\x03 \x01(\tR\x0c\x65rrorMessage\x12\x39\n\nupdated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tupdatedAt\x12:\n\x05phase\x18\x05 \x01(\x0e\x32$.officeconvertapi.v1.ConversionPhaseR\x05phase\x12)\n\x10\x63urrent_progress\x18\x06 \x01(\x05R\x0f\x63urrentProgress\x12!\n\x0cmax_progress\x18\x07 \x01(\x05R\x0bmaxProgress\":\n\x13GetSlideDeckRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"U\n\x14GetSlideDeckResponse\x12=\n\nslide_deck\x18\x01 \x01(\x0b\x32\x1e.officeconvertapi.v1.SlideDeckR\tslideDeck\">\n\x17\x44\x65leteConversionRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"Y\n\x18\x44\x65leteConversionResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12\x18\n\x07\x64\x65leted\x18\x02 \x01(\x08R\x07\x64\x65leted*\xb2\x01\n\x10\x43onversionStatus\x12!\n\x1d\x43ONVERSION_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x43ONVERSION_STATUS_PENDING\x10\x01\x12\x1d\n\x19\x43ONVERSION_STATUS_RUNNING\x10\x02\x12\x1f\n\x1b\x43ONVERSION_STATUS_SUCCEEDED\x10\x03\x12\x1c\n\x18\x43ONVERSION_STATUS_FAILED\x10\x04*\xe7\x01\n\x0f\x43onversionPhase\x12 \n\x1c\x43ONVERSION_PHASE_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x43ONVERSION_PHASE_INACTIVE\x10\x01\x12%\n!CONVERSION_PHASE_EXTRACTING_NOTES\x10\x02\x12 \n\x1c\x43ONVERSION_PHASE_PPTX_TO_PDF\x10\x03\x12\"\n\x1e\x43ONVERSION_PHASE_PDF_TO_IMAGES\x10\x04\x12&\n\"CONVERSION_PHASE_UPLOADING_RESULTS\x10\x05*\xd6\x01\n\x14\x43onversionResolution\x12%\n!CONVERSION_RESOLUTION_UNSPECIFIED\x10\x00\x12\x1c\n\x18\x43ONVERSION_RESOLUTION_SD\x10\x01\x12\x1c\n\x18\x43ONVERSION_RESOLUTION_HD\x10\x02\x12\x1d\n\x19\x43ONVERSION_RESOLUTION_FHD\x10\x03\x12\x1d\n\x19\x43ONVERSION_RESOLUTION_QHD\x10\x04\x12\x1d\n\x19\x43ONVERSION_RESOLUTION_UHD\x10\x05\x32\xcc\x04\n\x11\x43onversionService\x12q\n\x10\x43reateConversion\x12,.officeconvertapi.v1.CreateConversionRequest\x1a-.officeconvertapi.v1.CreateConversionResponse\"\x00\x12n\n\x0fStartConversion\x12+.officeconvertapi.v1.StartConversionRequest\x1a,.officeconvertapi.v1.StartConversionResponse\"\x00\x12z\n\x13GetConversionStatus\x12/.officeconvertapi.v1.GetConversionStatusRequest\x1a\x30.officeconvertapi.v1.GetConversionStatusResponse\"\x00\x12\x65\n\x0cGetSlideDeck\x12(.officeconvertapi.v1.GetSlideDeckRequest\x1a).officeconvertapi.v1.GetSlideDeckResponse\"\x00\x12q\n\x10\x44\x65leteConversion\x12,.officeconvertapi.v1.DeleteConversionRequest\x1a-.officeconvertapi.v1.DeleteConversionResponse\"\x00\x42LZJgithub.com/end/officeconvert/gen/go/officeconvertapi/v1;officeconvertapiv1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -33,34 +33,36 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'officeconvertapi.v1.convers if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'ZJgithub.com/end/officeconvert/gen/go/officeconvertapi/v1;officeconvertapiv1' - _globals['_CONVERSIONSTATUS']._serialized_start=1621 - _globals['_CONVERSIONSTATUS']._serialized_end=1799 - _globals['_CONVERSIONPHASE']._serialized_start=1802 - _globals['_CONVERSIONPHASE']._serialized_end=2033 + _globals['_CONVERSIONSTATUS']._serialized_start=1743 + _globals['_CONVERSIONSTATUS']._serialized_end=1921 + _globals['_CONVERSIONPHASE']._serialized_start=1924 + _globals['_CONVERSIONPHASE']._serialized_end=2155 + _globals['_CONVERSIONRESOLUTION']._serialized_start=2158 + _globals['_CONVERSIONRESOLUTION']._serialized_end=2372 _globals['_SLIDE']._serialized_start=94 _globals['_SLIDE']._serialized_end=185 _globals['_SLIDEDECK']._serialized_start=188 - _globals['_SLIDEDECK']._serialized_end=388 - _globals['_CREATECONVERSIONREQUEST']._serialized_start=390 - _globals['_CREATECONVERSIONREQUEST']._serialized_end=456 - _globals['_CREATECONVERSIONRESPONSE']._serialized_start=459 - _globals['_CREATECONVERSIONRESPONSE']._serialized_end=693 - _globals['_STARTCONVERSIONREQUEST']._serialized_start=695 - _globals['_STARTCONVERSIONREQUEST']._serialized_end=756 - _globals['_STARTCONVERSIONRESPONSE']._serialized_start=758 - _globals['_STARTCONVERSIONRESPONSE']._serialized_end=883 - _globals['_GETCONVERSIONSTATUSREQUEST']._serialized_start=885 - _globals['_GETCONVERSIONSTATUSREQUEST']._serialized_end=950 - _globals['_GETCONVERSIONSTATUSRESPONSE']._serialized_start=953 - _globals['_GETCONVERSIONSTATUSRESPONSE']._serialized_end=1316 - _globals['_GETSLIDEDECKREQUEST']._serialized_start=1318 - _globals['_GETSLIDEDECKREQUEST']._serialized_end=1376 - _globals['_GETSLIDEDECKRESPONSE']._serialized_start=1378 - _globals['_GETSLIDEDECKRESPONSE']._serialized_end=1463 - _globals['_DELETECONVERSIONREQUEST']._serialized_start=1465 - _globals['_DELETECONVERSIONREQUEST']._serialized_end=1527 - _globals['_DELETECONVERSIONRESPONSE']._serialized_start=1529 - _globals['_DELETECONVERSIONRESPONSE']._serialized_end=1618 - _globals['_CONVERSIONSERVICE']._serialized_start=2036 - _globals['_CONVERSIONSERVICE']._serialized_end=2624 + _globals['_SLIDEDECK']._serialized_end=434 + _globals['_CREATECONVERSIONREQUEST']._serialized_start=437 + _globals['_CREATECONVERSIONREQUEST']._serialized_end=578 + _globals['_CREATECONVERSIONRESPONSE']._serialized_start=581 + _globals['_CREATECONVERSIONRESPONSE']._serialized_end=815 + _globals['_STARTCONVERSIONREQUEST']._serialized_start=817 + _globals['_STARTCONVERSIONREQUEST']._serialized_end=878 + _globals['_STARTCONVERSIONRESPONSE']._serialized_start=880 + _globals['_STARTCONVERSIONRESPONSE']._serialized_end=1005 + _globals['_GETCONVERSIONSTATUSREQUEST']._serialized_start=1007 + _globals['_GETCONVERSIONSTATUSREQUEST']._serialized_end=1072 + _globals['_GETCONVERSIONSTATUSRESPONSE']._serialized_start=1075 + _globals['_GETCONVERSIONSTATUSRESPONSE']._serialized_end=1438 + _globals['_GETSLIDEDECKREQUEST']._serialized_start=1440 + _globals['_GETSLIDEDECKREQUEST']._serialized_end=1498 + _globals['_GETSLIDEDECKRESPONSE']._serialized_start=1500 + _globals['_GETSLIDEDECKRESPONSE']._serialized_end=1585 + _globals['_DELETECONVERSIONREQUEST']._serialized_start=1587 + _globals['_DELETECONVERSIONREQUEST']._serialized_end=1649 + _globals['_DELETECONVERSIONRESPONSE']._serialized_start=1651 + _globals['_DELETECONVERSIONRESPONSE']._serialized_end=1740 + _globals['_CONVERSIONSERVICE']._serialized_start=2375 + _globals['_CONVERSIONSERVICE']._serialized_end=2963 # @@protoc_insertion_point(module_scope) diff --git a/gen/python/officeconvertapi/v1/conversion_pb2.pyi b/gen/python/officeconvertapi/v1/conversion_pb2.pyi index 23d7116..fc5f083 100644 --- a/gen/python/officeconvertapi/v1/conversion_pb2.pyi +++ b/gen/python/officeconvertapi/v1/conversion_pb2.pyi @@ -26,6 +26,15 @@ class ConversionPhase(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): CONVERSION_PHASE_PPTX_TO_PDF: _ClassVar[ConversionPhase] CONVERSION_PHASE_PDF_TO_IMAGES: _ClassVar[ConversionPhase] CONVERSION_PHASE_UPLOADING_RESULTS: _ClassVar[ConversionPhase] + +class ConversionResolution(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + CONVERSION_RESOLUTION_UNSPECIFIED: _ClassVar[ConversionResolution] + CONVERSION_RESOLUTION_SD: _ClassVar[ConversionResolution] + CONVERSION_RESOLUTION_HD: _ClassVar[ConversionResolution] + CONVERSION_RESOLUTION_FHD: _ClassVar[ConversionResolution] + CONVERSION_RESOLUTION_QHD: _ClassVar[ConversionResolution] + CONVERSION_RESOLUTION_UHD: _ClassVar[ConversionResolution] CONVERSION_STATUS_UNSPECIFIED: ConversionStatus CONVERSION_STATUS_PENDING: ConversionStatus CONVERSION_STATUS_RUNNING: ConversionStatus @@ -37,6 +46,12 @@ CONVERSION_PHASE_EXTRACTING_NOTES: ConversionPhase CONVERSION_PHASE_PPTX_TO_PDF: ConversionPhase CONVERSION_PHASE_PDF_TO_IMAGES: ConversionPhase CONVERSION_PHASE_UPLOADING_RESULTS: ConversionPhase +CONVERSION_RESOLUTION_UNSPECIFIED: ConversionResolution +CONVERSION_RESOLUTION_SD: ConversionResolution +CONVERSION_RESOLUTION_HD: ConversionResolution +CONVERSION_RESOLUTION_FHD: ConversionResolution +CONVERSION_RESOLUTION_QHD: ConversionResolution +CONVERSION_RESOLUTION_UHD: ConversionResolution class Slide(_message.Message): __slots__ = ("index", "notes_plain", "image_url") @@ -49,22 +64,28 @@ class Slide(_message.Message): def __init__(self, index: _Optional[int] = ..., notes_plain: _Optional[str] = ..., image_url: _Optional[str] = ...) -> None: ... class SlideDeck(_message.Message): - __slots__ = ("conversion_id", "source_filename", "slides", "created_at") + __slots__ = ("conversion_id", "source_filename", "slides", "created_at", "width", "height") CONVERSION_ID_FIELD_NUMBER: _ClassVar[int] SOURCE_FILENAME_FIELD_NUMBER: _ClassVar[int] SLIDES_FIELD_NUMBER: _ClassVar[int] CREATED_AT_FIELD_NUMBER: _ClassVar[int] + WIDTH_FIELD_NUMBER: _ClassVar[int] + HEIGHT_FIELD_NUMBER: _ClassVar[int] conversion_id: str source_filename: str slides: _containers.RepeatedCompositeFieldContainer[Slide] created_at: _timestamp_pb2.Timestamp - def __init__(self, conversion_id: _Optional[str] = ..., source_filename: _Optional[str] = ..., slides: _Optional[_Iterable[_Union[Slide, _Mapping]]] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + width: int + height: int + def __init__(self, conversion_id: _Optional[str] = ..., source_filename: _Optional[str] = ..., slides: _Optional[_Iterable[_Union[Slide, _Mapping]]] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., width: _Optional[int] = ..., height: _Optional[int] = ...) -> None: ... class CreateConversionRequest(_message.Message): - __slots__ = ("source_filename",) + __slots__ = ("source_filename", "resolution") SOURCE_FILENAME_FIELD_NUMBER: _ClassVar[int] + RESOLUTION_FIELD_NUMBER: _ClassVar[int] source_filename: str - def __init__(self, source_filename: _Optional[str] = ...) -> None: ... + resolution: ConversionResolution + def __init__(self, source_filename: _Optional[str] = ..., resolution: _Optional[_Union[ConversionResolution, str]] = ...) -> None: ... class CreateConversionResponse(_message.Message): __slots__ = ("conversion_id", "upload_bucket", "upload_object_key", "upload_url", "expires_at") diff --git a/proto/officeconvertapi/v1/conversion.proto b/proto/officeconvertapi/v1/conversion.proto index e6ebba0..4f0e41d 100644 --- a/proto/officeconvertapi/v1/conversion.proto +++ b/proto/officeconvertapi/v1/conversion.proto @@ -43,6 +43,16 @@ enum ConversionPhase { CONVERSION_PHASE_UPLOADING_RESULTS = 5; } +// ConversionResolution represents preset output quality targets. +enum ConversionResolution { + CONVERSION_RESOLUTION_UNSPECIFIED = 0; + CONVERSION_RESOLUTION_SD = 1; + CONVERSION_RESOLUTION_HD = 2; + CONVERSION_RESOLUTION_FHD = 3; + CONVERSION_RESOLUTION_QHD = 4; + CONVERSION_RESOLUTION_UHD = 5; +} + // Slide contains extracted notes and the rendered image URL for one slide. message Slide { int32 index = 1; @@ -56,11 +66,14 @@ message SlideDeck { string source_filename = 2; repeated Slide slides = 3; google.protobuf.Timestamp created_at = 4; + int32 width = 5; + int32 height = 6; } // CreateConversionRequest starts a conversion session. message CreateConversionRequest { string source_filename = 1; + ConversionResolution resolution = 2; } // CreateConversionResponse returns upload details for the session. diff --git a/python/packages/officeconvert/src/officeconvert/conversion.py b/python/packages/officeconvert/src/officeconvert/conversion.py index db35a8b..65f609d 100644 --- a/python/packages/officeconvert/src/officeconvert/conversion.py +++ b/python/packages/officeconvert/src/officeconvert/conversion.py @@ -5,6 +5,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass import logging +import math from pathlib import Path import subprocess from typing import Iterable @@ -27,6 +28,12 @@ class SlideDeckResult: source_filename: str slides: list[SlideArtifact] + width: int + height: int + inferred_dpi: int + pptx_to_pdf_timeout_s: int + pdf_to_images_timeout_s: int + pdf_to_images_page_timeout_s: int ProgressCallback = Callable[[str, int, int], None] @@ -36,6 +43,21 @@ PHASE_EXTRACTING_NOTES = "extracting_notes" PHASE_PPTX_TO_PDF = "pptx_to_pdf" PHASE_PDF_TO_IMAGES = "pdf_to_images" +RESOLUTION_SD = "sd" +RESOLUTION_HD = "hd" +RESOLUTION_FHD = "fhd" +RESOLUTION_QHD = "qhd" +RESOLUTION_UHD = "uhd" + +_SHORT_EDGE_PIXELS_BY_RESOLUTION = { + RESOLUTION_SD: 480, + RESOLUTION_HD: 720, + RESOLUTION_FHD: 1080, + RESOLUTION_QHD: 1440, + RESOLUTION_UHD: 2160, +} +_EMU_PER_INCH = 914400 + logger = logging.getLogger("uvicorn.error") @@ -108,6 +130,8 @@ def render_pdf_to_images( out_dir: Path, *, dpi: int = 72, + target_width: int | None = None, + target_height: int | None = None, image_format: str = "png", timeout_s: int = 120, total_pages: int | None = None, @@ -132,14 +156,24 @@ def render_pdf_to_images( """ if not pdf_path.exists(): raise FileNotFoundError(f"source PDF does not exist: {pdf_path}") + if (target_width is None) != (target_height is None): + raise ValueError("target_width and target_height must be provided together") + if target_width is not None and target_width <= 0: + raise ValueError("target_width must be greater than zero") + if target_height is not None and target_height <= 0: + raise ValueError("target_height must be greater than zero") out_dir.mkdir(parents=True, exist_ok=True) + scale_args: list[str] = [] + if target_width is not None and target_height is not None: + scale_args = ["-scale-to-x", str(target_width), "-scale-to-y", str(target_height)] if total_pages is None: prefix_path = out_dir / "slide" command = [ "pdftoppm", "-r", str(dpi), + *scale_args, f"-{image_format}", str(pdf_path.resolve()), str(prefix_path), @@ -156,7 +190,7 @@ def render_pdf_to_images( message = ( "Poppler rasterization timed out after " f"{timeout_s} seconds while rendering {pdf_path.name}; " - "increase conversion PDF render timeout cap or lower image DPI" + "increase conversion PDF render timeout cap or lower output resolution" ) logger.error(message, exc_info=True) raise ConversionTimeoutError(message) from exc @@ -175,6 +209,7 @@ def render_pdf_to_images( "pdftoppm", "-r", str(dpi), + *scale_args, f"-{image_format}", "-f", str(page_index), @@ -202,7 +237,7 @@ def render_pdf_to_images( message = ( "Poppler rasterization timed out while rendering page " f"{page_index}/{total_pages} of {pdf_path.name}; " - f"{timeout_context}. Increase timeout settings or lower image DPI." + f"{timeout_context}. Increase timeout settings or lower output resolution." ) logger.error(message, exc_info=True) raise ConversionTimeoutError(message) from exc @@ -254,7 +289,7 @@ def convert_pptx_to_slidedeck( pptx_path: Path, work_dir: Path, *, - dpi: int = 72, + resolution: str = RESOLUTION_FHD, image_format: str = "png", pptx_to_pdf_timeout_s: int = 180, pdf_to_images_timeout_s: int = 1800, @@ -273,7 +308,7 @@ def convert_pptx_to_slidedeck( Args: pptx_path: Source `.pptx` path. work_dir: Scratch directory for generated outputs. - dpi: Rasterization DPI for output slide images. + resolution: Output resolution preset (`sd`, `hd`, `fhd`, `qhd`, `uhd`). image_format: Output image format accepted by `pdftoppm`. pptx_to_pdf_timeout_s: Timeout in seconds for the LibreOffice subprocess. pdf_to_images_timeout_s: Timeout in seconds for the Poppler subprocess. @@ -292,6 +327,18 @@ def convert_pptx_to_slidedeck( _emit_progress(progress_callback, PHASE_EXTRACTING_NOTES, 0, 1) notes = extract_slide_notes(pptx_path) _emit_progress(progress_callback, PHASE_EXTRACTING_NOTES, 1, 1) + slide_width, slide_height = _read_slide_size_emu(pptx_path) + output_width, output_height = _infer_output_dimensions_from_slide_size( + slide_width=slide_width, + slide_height=slide_height, + resolution=resolution, + ) + inferred_dpi = infer_minimum_raster_dpi( + slide_width_emu=slide_width, + slide_height_emu=slide_height, + output_width_px=output_width, + output_height_px=output_height, + ) slide_count = len(notes) pptx_to_pdf_timeout = _compute_adaptive_timeout( slide_count=slide_count, @@ -317,12 +364,16 @@ def convert_pptx_to_slidedeck( base_timeout_s=pdf_to_images_base_timeout_s, ) logger.info( - "Conversion plan source=%s slides=%d dpi=%d image_format=%s " + "Conversion plan source=%s slides=%d inferred_dpi=%d image_format=%s " + "resolution=%s output_size=%dx%d " "computed_timeouts_s[pptx_to_pdf_total=%d,pdf_to_images_total=%d,pdf_to_images_per_page=%d]", pptx_path.name, slide_count, - dpi, + inferred_dpi, image_format, + resolution, + output_width, + output_height, pptx_to_pdf_timeout, pdf_to_images_timeout, pdf_to_images_page_timeout, @@ -330,7 +381,9 @@ def convert_pptx_to_slidedeck( image_paths = render_pdf_to_images( pdf_path, image_dir, - dpi=dpi, + dpi=inferred_dpi, + target_width=output_width, + target_height=output_height, image_format=image_format, timeout_s=pdf_to_images_page_timeout, total_pages=slide_count, @@ -353,7 +406,82 @@ def convert_pptx_to_slidedeck( SlideArtifact(index=index, image_path=image_path, notes_plain=note) for index, (image_path, note) in enumerate(zip(image_paths, notes), start=1) ] - return SlideDeckResult(source_filename=pptx_path.name, slides=slides) + return SlideDeckResult( + source_filename=pptx_path.name, + slides=slides, + width=output_width, + height=output_height, + inferred_dpi=inferred_dpi, + pptx_to_pdf_timeout_s=pptx_to_pdf_timeout, + pdf_to_images_timeout_s=pdf_to_images_timeout, + pdf_to_images_page_timeout_s=pdf_to_images_page_timeout, + ) + + +def infer_output_dimensions_for_resolution( + pptx_path: Path, + *, + resolution: str, +) -> tuple[int, int]: + """Infer output image dimensions from source slide aspect ratio and preset.""" + slide_width, slide_height = _read_slide_size_emu(pptx_path) + return _infer_output_dimensions_from_slide_size( + slide_width=slide_width, + slide_height=slide_height, + resolution=resolution, + ) + + +def infer_minimum_raster_dpi( + *, + slide_width_emu: int, + slide_height_emu: int, + output_width_px: int, + output_height_px: int, +) -> int: + """Compute the minimum DPI needed to reach target output dimensions.""" + if slide_width_emu <= 0 or slide_height_emu <= 0: + raise ValueError("source slide dimensions must be greater than zero") + if output_width_px <= 0 or output_height_px <= 0: + raise ValueError("output dimensions must be greater than zero") + dpi_for_width = (output_width_px * _EMU_PER_INCH) / slide_width_emu + dpi_for_height = (output_height_px * _EMU_PER_INCH) / slide_height_emu + return max(1, math.ceil(max(dpi_for_width, dpi_for_height))) + + +def _read_slide_size_emu(pptx_path: Path) -> tuple[int, int]: + """Read presentation slide size in English Metric Units (EMU).""" + if not pptx_path.exists(): + raise FileNotFoundError(f"source PPTX does not exist: {pptx_path}") + presentation = Presentation(str(pptx_path.resolve())) + # Canonical python-pptx API on `Presentation` object. + slide_width = presentation.slide_width + slide_height = presentation.slide_height + if slide_width is None or slide_height is None: + raise ValueError("source presentation did not define slide dimensions") + slide_width = int(slide_width) + slide_height = int(slide_height) + if slide_width <= 0 or slide_height <= 0: + raise ValueError("source slide dimensions must be greater than zero") + return slide_width, slide_height + + +def _infer_output_dimensions_from_slide_size( + *, + slide_width: int, + slide_height: int, + resolution: str, +) -> tuple[int, int]: + """Infer output dimensions from slide size and short-edge preset.""" + normalized = resolution.strip().lower() + short_edge_pixels = _SHORT_EDGE_PIXELS_BY_RESOLUTION.get(normalized) + if short_edge_pixels is None: + raise ValueError(f"unsupported resolution preset: {resolution}") + if slide_width >= slide_height: + long_edge = max(1, round(short_edge_pixels * (slide_width / slide_height))) + return long_edge, short_edge_pixels + long_edge = max(1, round(short_edge_pixels * (slide_height / slide_width))) + return short_edge_pixels, long_edge def _compute_adaptive_timeout( diff --git a/python/packages/server/src/officeconvert_server/config.py b/python/packages/server/src/officeconvert_server/config.py index c2fd76a..bf3643e 100644 --- a/python/packages/server/src/officeconvert_server/config.py +++ b/python/packages/server/src/officeconvert_server/config.py @@ -16,7 +16,6 @@ class ServerConfig: s3_secure: bool s3_public_endpoint: str s3_session_ttl_seconds: int - conversion_image_dpi: int conversion_pptx_to_pdf_timeout_seconds: int conversion_pdf_to_images_timeout_seconds: int conversion_pptx_to_pdf_base_timeout_seconds: int @@ -35,7 +34,6 @@ def load_server_config() -> ServerConfig: s3_secure=os.getenv("S3_USE_SSL", "false").lower() == "true", s3_public_endpoint=os.getenv("S3_PUBLIC_ENDPOINT", "localhost:8333"), s3_session_ttl_seconds=int(os.getenv("S3_SESSION_TTL_SECONDS", "3600")), - conversion_image_dpi=int(os.getenv("CONVERSION_IMAGE_DPI", "72")), conversion_pptx_to_pdf_timeout_seconds=int( os.getenv("CONVERSION_PPTX_TO_PDF_TIMEOUT_SECONDS", "180") ), diff --git a/python/packages/server/src/officeconvert_server/models.py b/python/packages/server/src/officeconvert_server/models.py index 32b9946..6b1d6e2 100644 --- a/python/packages/server/src/officeconvert_server/models.py +++ b/python/packages/server/src/officeconvert_server/models.py @@ -19,6 +19,7 @@ class ConversionSession: conversion_id: str source_filename: str + resolution: conversion_pb2.ConversionResolution bucket_name: str upload_object_key: str status: conversion_pb2.ConversionStatus diff --git a/python/packages/server/src/officeconvert_server/service.py b/python/packages/server/src/officeconvert_server/service.py index 540ca30..33d2228 100644 --- a/python/packages/server/src/officeconvert_server/service.py +++ b/python/packages/server/src/officeconvert_server/service.py @@ -22,6 +22,11 @@ from officeconvert.conversion import ( PHASE_EXTRACTING_NOTES, PHASE_PDF_TO_IMAGES, PHASE_PPTX_TO_PDF, + RESOLUTION_FHD, + RESOLUTION_HD, + RESOLUTION_QHD, + RESOLUTION_SD, + RESOLUTION_UHD, ) from officeconvertapi.v1 import conversion_connect, conversion_pb2 @@ -31,6 +36,14 @@ from officeconvert_server.storage import S3Store logger = logging.getLogger("uvicorn.error") +_RESOLUTION_PRESET_BY_PROTO = { + conversion_pb2.CONVERSION_RESOLUTION_SD: RESOLUTION_SD, + conversion_pb2.CONVERSION_RESOLUTION_HD: RESOLUTION_HD, + conversion_pb2.CONVERSION_RESOLUTION_FHD: RESOLUTION_FHD, + conversion_pb2.CONVERSION_RESOLUTION_QHD: RESOLUTION_QHD, + conversion_pb2.CONVERSION_RESOLUTION_UHD: RESOLUTION_UHD, +} + class ConversionServiceImpl(conversion_connect.ConversionService): """Implements the conversion API with in-memory state and S3 orchestration.""" @@ -54,6 +67,11 @@ class ConversionServiceImpl(conversion_connect.ConversionService): raise ConnectError(Code.INVALID_ARGUMENT, "source_filename is required") if not source_filename.lower().endswith(".pptx"): raise ConnectError(Code.INVALID_ARGUMENT, "only .pptx input is supported") + resolution = request.resolution + if resolution == conversion_pb2.CONVERSION_RESOLUTION_UNSPECIFIED: + resolution = conversion_pb2.CONVERSION_RESOLUTION_FHD + if resolution not in _RESOLUTION_PRESET_BY_PROTO: + raise ConnectError(Code.INVALID_ARGUMENT, "resolution is invalid") conversion_id = str(uuid.uuid4()) bucket_name = f"oc-{conversion_id}" @@ -70,6 +88,7 @@ class ConversionServiceImpl(conversion_connect.ConversionService): session = ConversionSession( conversion_id=conversion_id, source_filename=source_filename, + resolution=resolution, bucket_name=bucket_name, upload_object_key=upload_key, status=conversion_pb2.CONVERSION_STATUS_PENDING, @@ -186,11 +205,11 @@ class ConversionServiceImpl(conversion_connect.ConversionService): """Execute conversion flow and persist terminal state in memory.""" started_at = time.monotonic() logger.info( - "Starting conversion conversion_id=%s source_filename=%s dpi=%d " + "Starting conversion conversion_id=%s source_filename=%s resolution=%s " "timeout_caps_s[pptx_to_pdf_total=%d,pdf_to_images_total=%d]", session.conversion_id, session.source_filename, - self._config.conversion_image_dpi, + conversion_pb2.ConversionResolution.Name(session.resolution), self._config.conversion_pptx_to_pdf_timeout_seconds, self._config.conversion_pdf_to_images_timeout_seconds, ) @@ -210,7 +229,7 @@ class ConversionServiceImpl(conversion_connect.ConversionService): convert_pptx_to_slidedeck, source_path, work_dir, - dpi=self._config.conversion_image_dpi, + resolution=_RESOLUTION_PRESET_BY_PROTO[session.resolution], pptx_to_pdf_timeout_s=self._config.conversion_pptx_to_pdf_timeout_seconds, pdf_to_images_timeout_s=self._config.conversion_pdf_to_images_timeout_seconds, pptx_to_pdf_base_timeout_s=self._config.conversion_pptx_to_pdf_base_timeout_seconds, @@ -224,6 +243,20 @@ class ConversionServiceImpl(conversion_connect.ConversionService): max_progress=max_value, ), ) + logger.info( + "Resolved conversion plan conversion_id=%s source_filename=%s " + "resolution=%s inferred_dpi=%d output_size=%dx%d " + "computed_timeouts_s[pptx_to_pdf_total=%d,pdf_to_images_total=%d,pdf_to_images_per_page=%d]", + session.conversion_id, + session.source_filename, + conversion_pb2.ConversionResolution.Name(session.resolution), + result.inferred_dpi, + result.width, + result.height, + result.pptx_to_pdf_timeout_s, + result.pdf_to_images_timeout_s, + result.pdf_to_images_page_timeout_s, + ) self._set_session_progress( session, phase=conversion_pb2.CONVERSION_PHASE_UPLOADING_RESULTS, @@ -235,6 +268,8 @@ class ConversionServiceImpl(conversion_connect.ConversionService): session, result.slides, result.source_filename, + result.width, + result.height, lambda current, max_value: self._set_session_progress( session, phase=conversion_pb2.CONVERSION_PHASE_UPLOADING_RESULTS, @@ -300,6 +335,8 @@ class ConversionServiceImpl(conversion_connect.ConversionService): session: ConversionSession, slides: list[SlideArtifact], source_filename: str, + width: int, + height: int, progress_callback: Callable[[int, int], None] | None = None, ) -> conversion_pb2.SlideDeck: """Upload generated slide images and construct API response payload.""" @@ -328,6 +365,8 @@ class ConversionServiceImpl(conversion_connect.ConversionService): source_filename=source_filename, slides=response_slides, created_at=_to_timestamp(utc_now()), + width=width, + height=height, ) async def _delayed_cleanup(self, session: ConversionSession) -> None: