Class: Mathpix::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/mathpix/client.rb

Overview

Core HTTP client for Mathpix API

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = Mathpix.configuration) ⇒ Client

Returns a new instance of Client.



8
9
10
11
# File 'lib/mathpix/client.rb', line 8

def initialize(config = Mathpix.configuration)
  @config = config
  config.validate!
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



6
7
8
# File 'lib/mathpix/client.rb', line 6

def config
  @config
end

Instance Method Details

#convert_document(document_path:, document_type:, **options) ⇒ String

Convert document (PDF, DOCX, PPTX) asynchronously

Examples:

conversion_id = client.convert_document(
  document_path: 'paper.pdf',
  document_type: :pdf,
  formats: [:markdown, :latex]
)

Parameters:

  • document_path (String)

    path to document file

  • document_type (Symbol)

    :pdf, :docx, :pptx

  • options (Hash)

    conversion options

Returns:

  • (String)

    conversion_id for polling

Raises:



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/mathpix/client.rb', line 106

def convert_document(document_path:, document_type:, **options)
  conversion_formats = build_conversion_formats(options)
  request_options = build_document_options(options)

  # The /v3/pdf endpoint takes a remote PDF via the `url` field, or a local
  # file via multipart upload — NOT the base64 `src` field used by the image
  # (/v3/text) endpoint. Sending `src` made Mathpix reply "Missing URL in
  # request body", which previously surfaced as a useless generic
  # "Client error".
  response =
    if url?(document_path)
      post('/pdf', { url: document_path, conversion_formats: conversion_formats }.merge(request_options))
    else
      post_multipart('/pdf', document_path, { conversion_formats: conversion_formats }.merge(request_options))
    end

  pdf_id = response['pdf_id']
  return pdf_id if pdf_id

  # 200 OK with an error body (missing/invalid fields, etc.)
  raise APIError.new(
    "Document submission failed: #{extract_error_message(response) || 'no pdf_id returned by Mathpix'}",
    status: 200,
    details: response.is_a?(Hash) ? response : {}
  )
end

#convert_strokes(strokes, **options) ⇒ Result

Convert handwritten strokes to text/LaTeX via /v3/strokes. Strokes arrive as [[[x,y],...], ...]; Mathpix wants parallel x/y arrays, so we transpose.

Parameters:

  • strokes (Array<Array<Array<Numeric>>>)

Returns:



45
46
47
48
49
50
51
52
53
# File 'lib/mathpix/client.rb', line 45

def convert_strokes(strokes, **options)
  pts = Array(strokes).map { |stroke| Array(stroke) }
  response = post('/strokes',
                  strokes: { strokes: { x: pts.map { |s| s.map { |p| p[0] } },
                                        y: pts.map { |s| s.map { |p| p[1] } } } },
                  formats: (options[:formats] || config.default_formats).map(&:to_s),
                  **build_request_options(options))
  Result.new(response)
end

#download(url) ⇒ String

Download file from URL

Parameters:

  • url (String)

    download URL

Returns:

  • (String)

    file content as bytes



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/mathpix/client.rb', line 73

def download(url)
  uri = URI(url)
  request = Net::HTTP::Get.new(uri)
  request['app_id'] = config.app_id
  request['app_key'] = config.app_key
  request['User-Agent'] = config.user_agent

  response = make_request(uri, request)

  case response
  when Net::HTTPSuccess
    response.body
  else
    raise APIError.new(
      "Download failed: #{response.code}",
      status: response.code.to_i
    )
  end
end

#get(path, params: {}) ⇒ Hash

Make GET request

Parameters:

  • path (String)

    API endpoint path

  • params (Hash) (defaults to: {})

    query parameters

Returns:

  • (Hash)

    parsed response



419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/mathpix/client.rb', line 419

def get(path, params: {})
  uri = URI("#{config.endpoint}#{path}")
  uri.query = URI.encode_www_form(params) unless params.empty?

  request = Net::HTTP::Get.new(uri)
  request['app_id'] = config.app_id
  request['app_key'] = config.app_key
  request['User-Agent'] = config.user_agent

  response = make_request(uri, request)
  handle_response(response)
end

#get_document_output(conversion_id, format) ⇒ String

Fetch a rendered document output (e.g. 'mmd', 'md', 'html', 'tex')

The /v3/pdf/id.ext endpoints return the raw converted content; the status endpoint (get_document_status) never contains it.

Parameters:

  • conversion_id (String)

    document conversion ID

  • format (String)

    output extension (mmd, md, html, tex, ...)

Returns:

  • (String)

    raw output content

Raises:



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/mathpix/client.rb', line 149

def get_document_output(conversion_id, format)
  uri = URI("#{config.endpoint}/pdf/#{conversion_id}.#{format}")
  request = Net::HTTP::Get.new(uri)
  request['app_id'] = config.app_id
  request['app_key'] = config.app_key
  request['User-Agent'] = config.user_agent

  response = make_request(uri, request)
  # Net::HTTP returns ASCII-8BIT bodies; Mathpix text outputs are UTF-8.
  return response.body.to_s.dup.force_encoding(Encoding::UTF_8) if response.is_a?(Net::HTTPSuccess)

  error_data = begin
    JSON.parse(response.body)
  rescue StandardError
    {}
  end
  raise APIError.new(
    "Failed to fetch '#{format}' output: #{extract_error_message(error_data) || "HTTP #{response.code}"}",
    status: response.code.to_i,
    details: error_data.is_a?(Hash) ? error_data : {}
  )
end

#get_document_status(conversion_id) ⇒ Hash

Get document conversion status

Parameters:

  • conversion_id (String)

    document conversion ID

Returns:

  • (Hash)

    status data



137
138
139
# File 'lib/mathpix/client.rb', line 137

def get_document_status(conversion_id)
  get("/pdf/#{conversion_id}")
end

#post(path, body) ⇒ Hash

Make POST request

Parameters:

  • path (String)

    API endpoint path

  • body (Hash)

    request body

Returns:

  • (Hash)

    parsed response



374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/mathpix/client.rb', line 374

def post(path, body)
  uri = URI("#{config.endpoint}#{path}")
  request = Net::HTTP::Post.new(uri)
  request['Content-Type'] = 'application/json'
  request['app_id'] = config.app_id
  request['app_key'] = config.app_key
  request['User-Agent'] = config.user_agent
  request.body = JSON.generate(body)

  response = make_request(uri, request)
  handle_response(response)
end

#recent(limit: 10) ⇒ Array<Result>

Get recent captures. /v3/ocr-results returns rows under "ocr_results", each nesting the OCR payload under "result" with the timestamp on top.

Parameters:

  • limit (Integer) (defaults to: 10)

    number of results

Returns:



60
61
62
63
64
65
66
67
# File 'lib/mathpix/client.rb', line 60

def recent(limit: 10)
  rows = get('/ocr-results', params: { per_page: limit })['ocr_results'] || []
  rows.map do |row|
    payload = row['result'] || row
    payload = payload.merge('timestamp' => row['timestamp']) if payload.is_a?(Hash) && row['timestamp']
    Result.new(payload)
  end
end

#snap(image_path_or_url, **options) ⇒ Result

Snap image to equation (core method)

Supports both local file paths and remote URLs

Examples:

Local file

client.snap('equation.png')

Remote URL

client.snap('https://example.com/equation.png')
client.snap(url: 'https://example.com/equation.png')

With options

client.snap('equation.png', formats: [:latex, :mathml])

Parameters:

  • image_path_or_url (String, Hash)

    path to image, URL, or hash with :path/:url key

  • options (Hash)

    request options

Returns:



27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/mathpix/client.rb', line 27

def snap(image_path_or_url, **options)
  src, source_ref = prepare_image_source(image_path_or_url, options)

  response = post('/text', {
                    src: src,
                    formats: (options[:formats] || config.default_formats).map(&:to_s),
                    include_line_data: options[:include_line_data] || false,
                    **build_request_options(options)
                  })

  Result.new(response, source_ref)
end