slidge-whatsapp 0.2.2__cp313-cp313-manylinux_2_36_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,542 @@
1
+ package media
2
+
3
+ import (
4
+ // Standard library.
5
+ "bytes"
6
+ "context"
7
+ "fmt"
8
+ "image"
9
+ "image/gif"
10
+ "image/jpeg"
11
+ "image/png"
12
+ "math"
13
+ "os"
14
+ "strconv"
15
+ "strings"
16
+ "time"
17
+
18
+ // Third-party packages.
19
+ "github.com/h2non/filetype"
20
+ "golang.org/x/image/draw"
21
+ "golang.org/x/image/webp"
22
+ )
23
+
24
+ // MIMEType represents a the media type for a data buffer. In general, values given concrete [MIMEType]
25
+ // identities are meant to be handled as targets for conversion and metadata extraction -- all other
26
+ // formats are handled on a best-case basis.
27
+ type MIMEType string
28
+
29
+ // BaseMediaType returns the media type without any additional parameters.
30
+ func (t MIMEType) BaseMediaType() MIMEType {
31
+ return MIMEType(strings.SplitN(string(t), ";", 2)[0])
32
+ }
33
+
34
+ const (
35
+ // The fallback MIME type when no concrete MIME type applies.
36
+ TypeUnknown MIMEType = "application/octet-stream"
37
+
38
+ // Audio formats.
39
+ TypeM4A MIMEType = "audio/mp4"
40
+ TypeOgg MIMEType = "audio/ogg"
41
+
42
+ // Video formats.
43
+ TypeMP4 MIMEType = "video/mp4"
44
+ TypeWebM MIMEType = "video/webm"
45
+
46
+ // Image formats.
47
+ TypeJPEG MIMEType = "image/jpeg"
48
+ TypePNG MIMEType = "image/png"
49
+ TypeGIF MIMEType = "image/gif"
50
+ TypeWebP MIMEType = "image/webp"
51
+
52
+ // Document formats.
53
+ TypePDF MIMEType = "application/pdf"
54
+ )
55
+
56
+ // DetectMIME returns a valid MIME type, as inferred by the data given (usually the first few bytes)
57
+ // or [TypeUnknown] if no valid MIME type could be inferred.
58
+ func DetectMIMEType(data []byte) MIMEType {
59
+ switch t, _ := filetype.Match(data); t.MIME.Value {
60
+ case "audio/m4a":
61
+ return TypeM4A // Correct `audio/m4a` to its valid sub-type.
62
+ case "":
63
+ return TypeUnknown
64
+ default:
65
+ return MIMEType(t.MIME.Value)
66
+ }
67
+ }
68
+
69
+ // AudioCodec represents the encoding method used for an audio stream.
70
+ type AudioCodec string
71
+
72
+ // VideoCodec represents the encoding method used for a video stream.
73
+ type VideoCodec string
74
+
75
+ const (
76
+ // Audio codecs.
77
+ CodecOpus AudioCodec = "opus"
78
+ CodecAAC AudioCodec = "aac"
79
+
80
+ // Video codecs.
81
+ CodecH264 VideoCodec = "h264"
82
+ )
83
+
84
+ // Error messages.
85
+ const (
86
+ errInvalidCodec = "media with MIME type %s only support codec %s currently, invalid codec %s chosen"
87
+ )
88
+
89
+ // Spec represents the description of a target media file; depending on platform support and media
90
+ // conversion intricacies, source media files can be of any type, even types that aren't represented
91
+ // here. Nevertheless, it is intended that media types and codecs represented here are valid as both
92
+ // input and output formats.
93
+ type Spec struct {
94
+ // Required parameters.
95
+ MIME MIMEType // The MIME type for the target media.
96
+
97
+ // Optional parameters.
98
+ AudioCodec AudioCodec // The codec to use for the audio stream, must be correct for the MIME type given.
99
+ AudioChannels int // The number of channels for the audio stream, 1 for mono, 2 for stereo.
100
+ AudioBitRate int // The bit rate for the audio stream, in kBit/second.
101
+ AudioSampleRate int // The sample-rate frequency for the audio stream, common values are 44100, 48000.
102
+
103
+ VideoCodec VideoCodec // The codec to use for the video stream, must be correct for the MIME type given.
104
+ VideoPixelFormat string // The pixel format used for video stream, typically 'yub420p' for MP4.
105
+ VideoFrameRate int // The frame rate for the video stream, in frames/second.
106
+ VideoWidth int // The width of the video stream, in pixels.
107
+ VideoHeight int // The height of the video stream, in pixels.
108
+ VideoFilter string // A complex filter to apply to the video stream.
109
+
110
+ ImageWidth int // The width of the image, in pixels.
111
+ ImageHeight int // The height of the image, in pixels.
112
+ ImageQuality int // Image quality for lossy image formats, typically a value from 1 to 100.
113
+ ImageFrameRate int // The frame-rate for animated images.
114
+
115
+ DocumentPage int // The number of pages for the document.
116
+
117
+ Duration time.Duration // The duration of the audio or video stream.
118
+ StripMetadata bool // Whether or not to remove any container-level metadata present in the stream.
119
+ }
120
+
121
+ // CommandLineArgs returns the current [Spec] as a list of command-line arguments meant for FFMPEG
122
+ // invocations. Where the specification is missing values, default values will be filled where
123
+ // necessary; however, invalid values may have this function return errors.
124
+ func (s Spec) commandLineArgs() ([]string, error) {
125
+ var args []string
126
+ var mime = s.MIME.BaseMediaType()
127
+
128
+ switch mime {
129
+ case TypeOgg, TypeM4A:
130
+ // Audio file format parameters.
131
+ switch mime {
132
+ case TypeOgg:
133
+ if s.AudioCodec != "" && s.AudioCodec != CodecOpus {
134
+ return nil, fmt.Errorf(errInvalidCodec, mime, CodecOpus, s.AudioCodec)
135
+ }
136
+ args = append(args, "-f", "ogg", "-c:a", "libopus")
137
+ case TypeM4A:
138
+ if s.AudioCodec != "" && s.AudioCodec != CodecAAC {
139
+ return nil, fmt.Errorf(errInvalidCodec, mime, CodecAAC, s.AudioCodec)
140
+ }
141
+ args = append(args, "-f", "ipod", "-c:a", "aac")
142
+ }
143
+
144
+ if s.AudioChannels > 0 {
145
+ args = append(args, "-ac", strconv.Itoa(s.AudioChannels))
146
+ }
147
+ if s.AudioBitRate > 0 {
148
+ args = append(args, "-b:a", strconv.Itoa(s.AudioBitRate)+"k")
149
+ }
150
+ if s.AudioSampleRate > 0 {
151
+ args = append(args, "-ar", strconv.Itoa(s.AudioSampleRate))
152
+ }
153
+ case TypeMP4:
154
+ // Video file format parameters.
155
+ if s.VideoCodec != "" && s.VideoCodec != CodecH264 {
156
+ return nil, fmt.Errorf(errInvalidCodec, mime, CodecH264, s.VideoCodec)
157
+ } else if s.AudioCodec != "" && s.AudioCodec != CodecAAC {
158
+ return nil, fmt.Errorf(errInvalidCodec, mime, CodecAAC, s.AudioCodec)
159
+ }
160
+
161
+ // Set input image frame-rate, e.g. when converting from GIF to MP4.
162
+ if s.ImageFrameRate > 0 {
163
+ args = append(args, "-r", strconv.Itoa(s.ImageFrameRate))
164
+ }
165
+
166
+ args = append(args,
167
+ "-f", "mp4", "-c:v", "libx264", "-c:a", "aac",
168
+ "-profile:v", "baseline", // Use Baseline profile for better compatibility.
169
+ "-level", "3.0", // Ensure compatibility with older devices.
170
+ "-movflags", "+faststart", // Use Faststart for quicker rendering.
171
+ )
172
+
173
+ if s.VideoPixelFormat != "" {
174
+ args = append(args, "-pix_fmt", s.VideoPixelFormat)
175
+ }
176
+ if s.VideoFilter != "" {
177
+ args = append(args, "-filter:v", s.VideoFilter)
178
+ }
179
+ if s.VideoFrameRate > 0 {
180
+ args = append(args,
181
+ "-r", strconv.Itoa(s.VideoFrameRate),
182
+ "-g", strconv.Itoa(s.VideoFrameRate*2),
183
+ )
184
+ }
185
+ if s.AudioBitRate > 0 {
186
+ args = append(args, "-b:a", strconv.Itoa(s.AudioBitRate)+"k")
187
+ }
188
+ if s.AudioSampleRate > 0 {
189
+ args = append(args, "-r:a", strconv.Itoa(s.AudioSampleRate))
190
+ }
191
+ case TypeJPEG:
192
+ args = append(args, "-f", "mjpeg", "-qscale:v", "5", "-frames:v", "1")
193
+
194
+ // Scale thumbnail if width/height pixel factors given.
195
+ if s.ImageWidth > 0 || s.ImageHeight > 0 {
196
+ if s.ImageWidth == 0 {
197
+ s.ImageWidth = -1
198
+ } else if s.ImageHeight == 0 {
199
+ s.ImageHeight = -1
200
+ }
201
+ w, h := strconv.FormatInt(int64(s.ImageWidth), 10), strconv.FormatInt(int64(s.ImageHeight), 10)
202
+ args = append(args, "-vf", "scale="+w+":"+h)
203
+ }
204
+ default:
205
+ return nil, fmt.Errorf("cannot process media specification for empty or unknown MIME type")
206
+ }
207
+
208
+ if s.StripMetadata {
209
+ args = append(args, "-map_metadata", "-1")
210
+ }
211
+
212
+ return args, nil
213
+ }
214
+
215
+ // Convert processes the given data, assumed to represent a media file, according to the target
216
+ // specification given. For information on how these definitions affect media conversions, see the
217
+ // documentation for the [Spec] type.
218
+ func Convert(ctx context.Context, data []byte, spec *Spec) ([]byte, error) {
219
+ var from, to = DetectMIMEType(data), spec.MIME.BaseMediaType()
220
+ switch from {
221
+ case TypeOgg, TypeM4A:
222
+ switch to {
223
+ case TypeOgg, TypeM4A:
224
+ return convertAudioVideo(ctx, data, spec)
225
+ }
226
+ case TypeMP4, TypeWebM:
227
+ switch to {
228
+ case TypeMP4, TypeJPEG:
229
+ return convertAudioVideo(ctx, data, spec)
230
+ }
231
+ case TypeGIF:
232
+ switch to {
233
+ case TypeMP4:
234
+ return convertAudioVideo(ctx, data, spec)
235
+ case TypeJPEG, TypePNG:
236
+ return convertImage(ctx, data, spec)
237
+ }
238
+ case TypeJPEG, TypePNG, TypeWebP:
239
+ switch to {
240
+ case TypeJPEG, TypePNG:
241
+ return convertImage(ctx, data, spec)
242
+ }
243
+ case TypePDF:
244
+ switch to {
245
+ case TypeJPEG, TypePNG:
246
+ return convertDocument(ctx, data, spec)
247
+ }
248
+ }
249
+
250
+ return nil, fmt.Errorf("cannot convert file of type '%s' to '%s'", from, to)
251
+ }
252
+
253
+ // ConvertAudioVideo processes the given audio/video data via FFmpeg, for the target specification
254
+ // given. Calls to FFmpeg will be given arguments as per [Spec.commandLineArgs].
255
+ func convertAudioVideo(ctx context.Context, data []byte, spec *Spec) ([]byte, error) {
256
+ args, err := spec.commandLineArgs()
257
+ if err != nil {
258
+ return nil, err
259
+ }
260
+
261
+ in, err := createTempFile(data)
262
+ if err != nil {
263
+ return nil, err
264
+ }
265
+
266
+ defer os.Remove(in)
267
+
268
+ out, err := createTempFile(nil)
269
+ if err != nil {
270
+ return nil, err
271
+ }
272
+
273
+ defer os.Remove(out)
274
+
275
+ if err := ffmpeg(ctx, in, out, args...); err != nil {
276
+ return nil, err
277
+ }
278
+
279
+ return os.ReadFile(out)
280
+ }
281
+
282
+ // ConvertImage processes the following image data given via Go-native image processing. Currently,
283
+ // only JPEG and PNG output is allowed, as set in the [Spec.MIME] field.
284
+ func convertImage(_ context.Context, data []byte, spec *Spec) ([]byte, error) {
285
+ img, _, err := image.Decode(bytes.NewReader(data))
286
+ if err != nil {
287
+ return nil, err
288
+ }
289
+
290
+ return processImage(img, spec)
291
+ }
292
+
293
+ // ConvertDocument processes the given document, extracting [Spec.PageNumber] as an image of a MIME
294
+ // type corresponding to the given [Spec.MIME]. An error is returned if the data given is not a
295
+ // valid document, or if the page number requested does not exist.
296
+ func convertDocument(ctx context.Context, data []byte, spec *Spec) ([]byte, error) {
297
+ return internalConvertDocument(ctx, data, spec)
298
+ }
299
+
300
+ // ProcessImage handles processing and encoding for the given Go-native image representation.
301
+ func processImage(img image.Image, spec *Spec) ([]byte, error) {
302
+ // Resize image if dimensions given in spec, retaining aspect ratio if either width or height
303
+ // aren't provided.
304
+ if spec.ImageWidth > 0 || spec.ImageHeight > 0 {
305
+ width, height := spec.ImageWidth, spec.ImageHeight
306
+ if width == 0 {
307
+ width = int(float64(img.Bounds().Max.X) / (float64(img.Bounds().Max.Y) / float64(height)))
308
+ } else if height == 0 {
309
+ height = int(float64(img.Bounds().Max.Y) / (float64(img.Bounds().Max.X) / float64(width)))
310
+ }
311
+
312
+ tmp := image.NewRGBA(image.Rect(0, 0, width, height))
313
+ draw.ApproxBiLinear.Scale(tmp, tmp.Rect, img, img.Bounds(), draw.Over, nil)
314
+ img = tmp
315
+ }
316
+
317
+ var err error
318
+ var buf bytes.Buffer
319
+
320
+ // Re-encode image based on target MIME type.
321
+ switch spec.MIME.BaseMediaType() {
322
+ case TypeJPEG:
323
+ o := jpeg.Options{Quality: spec.ImageQuality}
324
+ if o.Quality == 0 {
325
+ o.Quality = jpeg.DefaultQuality
326
+ }
327
+
328
+ err = jpeg.Encode(&buf, img, nil)
329
+ case TypePNG:
330
+ err = png.Encode(&buf, img)
331
+ }
332
+
333
+ if err != nil {
334
+ return nil, err
335
+ }
336
+
337
+ return buf.Bytes(), nil
338
+ }
339
+
340
+ // GetSpec returns a media specification corresponding to the data given. The [Spec] value returned
341
+ // will only have its fields partially populated, as not all values can be derived accurately.
342
+ func GetSpec(ctx context.Context, data []byte) (*Spec, error) {
343
+ switch DetectMIMEType(data) {
344
+ case TypeJPEG, TypePNG, TypeGIF, TypeWebP:
345
+ return getImageSpec(ctx, data)
346
+ case TypePDF:
347
+ return getDocumentSpec(ctx, data)
348
+ default:
349
+ // Assume file is some form of audio or video file, and attempt best-effort extraction of spec.
350
+ return getAudioVideoSpec(ctx, data)
351
+ }
352
+ }
353
+
354
+ // GetAudioVideoSpec attempts to fetch as much metadata as possible from the given data buffer, which
355
+ // is assumed to be some form of audio of video file, via FFmpeg.
356
+ func getAudioVideoSpec(ctx context.Context, data []byte) (*Spec, error) {
357
+ in, err := createTempFile(data)
358
+ if err != nil {
359
+ return nil, err
360
+ }
361
+
362
+ defer os.Remove(in)
363
+ var result Spec
364
+
365
+ out, err := ffprobe(ctx, in,
366
+ "-show_entries", "stream=codec_name,width,height,sample_rate,duration",
367
+ )
368
+
369
+ if s, ok := out["streams"].([]any); ok {
370
+ if len(s) == 0 {
371
+ return nil, fmt.Errorf("no valid audio/video streams found in data")
372
+ } else if r, ok := s[0].(map[string]any); ok {
373
+ if v, ok := r["duration"].(string); ok {
374
+ if v, err := strconv.ParseFloat(v, 64); err == nil {
375
+ result.Duration = time.Duration(v * float64(time.Second))
376
+ }
377
+ }
378
+ if v, ok := r["width"].(string); ok {
379
+ if v, err := strconv.Atoi(v); err == nil {
380
+ result.VideoWidth = v
381
+ }
382
+ }
383
+ if v, ok := r["height"].(string); ok {
384
+ if v, err := strconv.Atoi(v); err == nil {
385
+ result.VideoHeight = v
386
+ }
387
+ }
388
+ if v, ok := r["sample_rate"].(string); ok {
389
+ if v, err := strconv.Atoi(v); err == nil {
390
+ result.AudioSampleRate = v
391
+ }
392
+ }
393
+ if v, ok := r["codec_name"].(string); ok {
394
+ if result.VideoWidth > 0 || result.VideoHeight > 0 {
395
+ result.VideoCodec = VideoCodec(v)
396
+ } else {
397
+ result.AudioCodec = AudioCodec(v)
398
+ }
399
+ }
400
+ }
401
+ }
402
+
403
+ return &result, nil
404
+ }
405
+
406
+ // GetImageSpec fetches as much metadata as possible from the given data buffer, which is assumed to
407
+ // be a valid image (e.g. a JPEG, PNG, GIF) file.
408
+ func getImageSpec(_ context.Context, data []byte) (*Spec, error) {
409
+ var err error
410
+ var config image.Config
411
+ var buf = bytes.NewReader(data)
412
+
413
+ switch DetectMIMEType(data) {
414
+ case TypeGIF:
415
+ dec, err := gif.DecodeAll(buf)
416
+ if err != nil {
417
+ return nil, err
418
+ }
419
+ var spec Spec
420
+ if len(dec.Image) > 1 {
421
+ var t float64
422
+ for d := range dec.Delay {
423
+ t += float64(d) / 100
424
+ }
425
+ if t > 0 {
426
+ spec.ImageFrameRate = int(float64(len(dec.Image)) / t)
427
+ }
428
+ }
429
+ s := dec.Image[0].Bounds().Max
430
+ spec.ImageWidth, spec.ImageHeight = s.X, s.Y
431
+ return &spec, nil
432
+ case TypeJPEG:
433
+ config, err = jpeg.DecodeConfig(buf)
434
+ case TypePNG:
435
+ config, err = png.DecodeConfig(buf)
436
+ case TypeWebP:
437
+ config, err = webp.DecodeConfig(buf)
438
+ }
439
+
440
+ if err != nil {
441
+ return nil, err
442
+ }
443
+
444
+ return &Spec{
445
+ ImageWidth: config.Width,
446
+ ImageHeight: config.Height,
447
+ }, nil
448
+ }
449
+
450
+ // GetDocumentSpec fetches as much metadata as possible from the given data buffer, which is assumed
451
+ // to be a valid PDF file.
452
+ func getDocumentSpec(ctx context.Context, data []byte) (*Spec, error) {
453
+ return internalGetDocumentSpec(ctx, data)
454
+ }
455
+
456
+ // GetWaveform returns a list of samples, scaled from 0 to 100, representing linear loudness values.
457
+ //
458
+ // An error will be returned if the [Spec] given has no sample-rate or duration corresponding to the
459
+ // data given, as both these values are necessary for deriving the number of samples.
460
+ //
461
+ // The number of samples returned will be equal to the given maximum number provided, and will be
462
+ // padded with 0 values if necessary.
463
+ func GetWaveform(ctx context.Context, data []byte, spec *Spec, maxSamples int) ([]byte, error) {
464
+ if spec.AudioSampleRate == 0 || spec.Duration == 0 {
465
+ return nil, fmt.Errorf("no sample-rate or duration for media given")
466
+ }
467
+
468
+ in, err := createTempFile(data)
469
+ if err != nil {
470
+ return nil, err
471
+ }
472
+
473
+ defer os.Remove(in)
474
+
475
+ // Determine number of waveform to take based on duration and sample-rate of original file.
476
+ numSamples := strconv.Itoa(int(float64(spec.AudioSampleRate)*spec.Duration.Seconds()) / maxSamples)
477
+ out, err := ffprobe(ctx,
478
+ "amovie="+in+",asetnsamples="+numSamples+",astats=metadata=1:reset=1",
479
+ "-f", "lavfi",
480
+ "-show_entries", "frame_tags=lavfi.astats.Overall.Peak_level",
481
+ )
482
+
483
+ // Get waveform with defined maximum number of samples, and scale these from a range of 0 to 100.
484
+ var samples = make([]byte, 0, maxSamples)
485
+ if f, ok := out["frames"].([]any); ok {
486
+ if len(f) == 0 {
487
+ return nil, fmt.Errorf("no audio frames found in media")
488
+ }
489
+ for i := range f {
490
+ if r, ok := f[i].(map[string]any); ok {
491
+ if t, ok := r["tags"].(map[string]any); ok {
492
+ if v, ok := t["lavfi.astats.Overall.Peak_level"].(string); ok {
493
+ db, err := strconv.ParseFloat(v, 64)
494
+ if err == nil {
495
+ samples = append(samples, byte(math.Pow(10, (db/50))*100))
496
+ }
497
+ }
498
+ }
499
+ }
500
+ }
501
+ }
502
+
503
+ return samples, nil
504
+ }
505
+
506
+ var (
507
+ // The default path for storing temporary files.
508
+ tempDir = os.TempDir()
509
+ )
510
+
511
+ // SetTempDirectory sets the global temporary directory used internally by media conversion commands.
512
+ func SetTempDirectory(path string) error {
513
+ if _, err := os.Stat(path); err != nil {
514
+ return err
515
+ }
516
+
517
+ tempDir = path
518
+ return nil
519
+ }
520
+
521
+ // CreateTempFile creates a temporary file in the pre-defined temporary directory (or the default,
522
+ // system-wide temporary directory, if no override value was set) and returns the absolute path for
523
+ // the file, or an error if none could be created.
524
+ func createTempFile(data []byte) (string, error) {
525
+ f, err := os.CreateTemp(tempDir, "media-*")
526
+ if err != nil {
527
+ return "", fmt.Errorf("failed creating temporary file: %w", err)
528
+ }
529
+
530
+ defer f.Close()
531
+ if len(data) > 0 {
532
+ if n, err := f.Write(data); err != nil {
533
+ os.Remove(f.Name())
534
+ return "", fmt.Errorf("failed writing to temporary file: %w", err)
535
+ } else if n < len(data) {
536
+ os.Remove(f.Name())
537
+ return "", fmt.Errorf("failed writing to temporary file: incomplete write, want %d, write %d bytes", len(data), n)
538
+ }
539
+ }
540
+
541
+ return f.Name(), nil
542
+ }
@@ -0,0 +1,47 @@
1
+ //go:build mupdf
2
+
3
+ package media
4
+
5
+ import (
6
+ // Standard library.
7
+ "context"
8
+ "fmt"
9
+
10
+ // Third-party packages.
11
+ "github.com/gen2brain/go-fitz"
12
+ )
13
+
14
+ // InternalConvertDocument converts the given data buffer, which is assumed to be a valid PDF document,
15
+ // into the target spec with MuPDF.
16
+ func internalConvertDocument(_ context.Context, data []byte, spec *Spec) ([]byte, error) {
17
+ doc, err := fitz.NewFromMemory(data)
18
+ if err != nil {
19
+ return nil, fmt.Errorf("failed reading document: %s", err)
20
+ }
21
+
22
+ defer doc.Close()
23
+
24
+ var buf []byte
25
+ if n := doc.NumPage(); n <= spec.DocumentPage {
26
+ return nil, fmt.Errorf("cannot read page %d in document with %d pages", spec.DocumentPage+1, n+1)
27
+ } else if img, err := doc.Image(spec.DocumentPage); err != nil {
28
+ if buf, err = processImage(img, spec); err != nil {
29
+ return nil, err
30
+ }
31
+ }
32
+
33
+ return buf, nil
34
+ }
35
+
36
+ // InternalGetDocumentSpec fetches as much metadata as possible from the given data buffer with MuPDF.
37
+ func internalGetDocumentSpec(_ context.Context, data []byte) (*Spec, error) {
38
+ doc, err := fitz.NewFromMemory(data)
39
+ if err != nil {
40
+ return nil, fmt.Errorf("failed to read document: %s", err)
41
+ }
42
+
43
+ defer doc.Close()
44
+ return &Spec{
45
+ DocumentPage: doc.NumPage(),
46
+ }, nil
47
+ }
@@ -0,0 +1,19 @@
1
+ //go:build !mupdf
2
+
3
+ package media
4
+
5
+ import (
6
+ // Standard library.
7
+ "context"
8
+ "errors"
9
+ )
10
+
11
+ // InternalGetDocumentSpec is a stub implementation, as called by [convertDocument].
12
+ func internalConvertDocument(_ context.Context, _ []byte, _ *Spec) ([]byte, error) {
13
+ return nil, errors.New("document support not enabled in this build")
14
+ }
15
+
16
+ // InternalGetDocumentSpec is a stub implementation, as called by [getDocumentSpec].
17
+ func internalGetDocumentSpec(_ context.Context, _ []byte) (*Spec, error) {
18
+ return nil, errors.New("document support not enabled in this build")
19
+ }