{"id":46510435,"url":"https://github.com/peterc/pure_jpeg","last_synced_at":"2026-03-11T21:01:08.109Z","repository":{"id":342405030,"uuid":"1173860484","full_name":"peterc/pure_jpeg","owner":"peterc","description":"Pure Ruby JPEG encoder and decoder with no native dependencies","archived":false,"fork":false,"pushed_at":"2026-03-06T15:08:42.000Z","size":993,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-08T20:04:45.146Z","etag":null,"topics":["graphics","jpeg","jpeg-encoder","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/peterc.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-05T20:36:15.000Z","updated_at":"2026-03-06T20:32:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/peterc/pure_jpeg","commit_stats":null,"previous_names":["peterc/pure_jpeg"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/peterc/pure_jpeg","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterc%2Fpure_jpeg","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterc%2Fpure_jpeg/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterc%2Fpure_jpeg/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterc%2Fpure_jpeg/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/peterc","download_url":"https://codeload.github.com/peterc/pure_jpeg/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterc%2Fpure_jpeg/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30297816,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-09T13:46:43.843Z","status":"ssl_error","status_checked_at":"2026-03-09T13:46:42.821Z","response_time":61,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["graphics","jpeg","jpeg-encoder","ruby"],"created_at":"2026-03-06T16:05:08.742Z","updated_at":"2026-03-09T19:01:34.704Z","avatar_url":"https://github.com/peterc.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"purejpeg.jpg\" width=\"480\" alt=\"PureJPEG\"\u003e\n\u003c/p\u003e\n\n# PureJPEG - Pure Ruby JPEG encoder and decoder library\n\nConvert PNG or other pixel data to JPEG. Or the other way! Implements baseline JPEG encoding (DCT, Huffman, 4:2:0 chroma subsampling) and decodes both baseline and progressive JPEGs. Exposes a variety of encoding options to adjust parts of the JPEG pipeline not normally available (I needed this to recreate the JPEG compression styles of older digital cameras - don't ask..)\n\nIt works on CRuby 3.0+, TruffleRuby 33.0, and JRuby 10.0.\n\n\u003e [!NOTE]\n\u003e Rubyists might find the [AI Disclosure](#ai-disclosure) section below of interest.\n\n## Installation\n\nYou know the drill: \n\n```ruby\ngem \"pure_jpeg\"\n```\n\n```\ngem install pure_jpeg\n```\n\nThere are no runtime dependencies. [ChunkyPNG](https://github.com/wvanbergen/chunky_png) is optional (though quite useful) if you want to use `from_chunky_png`. I have a pure PNG encoder/decoder not far behind this that will ultimately plug in nicely too to get 100% pure Ruby graphical bliss ;-)\n\n`examples/` contains some useful example scripts for basic JPEG to PNG and PNG to JPEG conversion if you want to do some quick tests without writing code.\n\n## Encoding (making JPEGs!)\n\n### From ChunkyPNG (easiest to get started)\n\n```ruby\nrequire \"chunky_png\"\nrequire \"pure_jpeg\"\n\nimage = ChunkyPNG::Image.from_file(\"photo.png\")\nPureJPEG.from_chunky_png(image, quality: 80).write(\"photo.jpg\")\n```\n\n### From any pixel source\n\nPureJPEG accepts any object that responds to `width`, `height`, and `[x, y]` (returning an object with `.r`, `.g`, `.b` in 0-255):\n\n```ruby\nrequire \"pure_jpeg\"\n\nencoder = PureJPEG.encode(source, quality: 85)\nencoder.write(\"output.jpg\")\n\n# Or get raw bytes\njpeg_data = encoder.to_bytes\n```\n\n### From raw pixel data\n\n```ruby\nsource = PureJPEG::Source::RawSource.new(width, height) do |x, y|\n  [r, g, b]  # return RGB values 0-255\nend\n\nPureJPEG.encode(source).write(\"output.jpg\")\n```\n\n### Grayscale\n\n```ruby\nPureJPEG.encode(source, grayscale: true).write(\"gray.jpg\")\n```\n\n### Encoder options\n\n```ruby\nPureJPEG.encode(source,\n  quality: 85,                    # 1-100, overall compression level\n  grayscale: false,               # single-channel grayscale mode\n  chroma_quality: nil,            # 1-100, independent Cb/Cr quality (defaults to quality)\n  luminance_table: nil,           # custom 64-element quantization table for Y\n  chrominance_table: nil,         # custom 64-element quantization table for Cb/Cr\n  quantization_modifier: nil,     # proc(table, :luminance/:chrominance) -\u003e modified table\n  scramble_quantization: false    # intentionally misordered quant tables (creative effect)\n)\n```\n\nSee [CREATIVE.md](CREATIVE.md) for detailed examples of the creative encoding options.\n\nHere's a quick example of sort of the \"old digital camera\" effect I was looking for though:\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003cstrong\u003eNormal\u003c/strong\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003cstrong\u003eScrambled quantization\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cimg src=\"examples/peppers.jpg\" width=\"360\"\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cimg src=\"examples/peppers-funky.jpg\" width=\"360\"\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\nAnd here's what happens when you convert a PNG with transparency — JPEG doesn't support alpha, so the hidden RGB data behind transparent pixels bleeds through:\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003cstrong\u003ePNG with transparency\u003c/strong\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003cstrong\u003eConverted to JPEG\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cimg src=\"examples/dice.png\" width=\"360\"\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cimg src=\"examples/dice.jpg\" width=\"360\"\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\nI consider this a feature but you may consider it a deficiency and that a default background of white should be applied. This may be something I'll add if anyone wants it!\n\nNote that each stage of the JPEG pipeline is a separate module, so individual components (DCT, quantization, Huffman coding) can be replaced or extended independently which is kinda my plan here as I made this to play around with effects.\n\n## Decoding (reading JPEGs!)\n\n### From file\n\n```ruby\nimage = PureJPEG.read(\"photo.jpg\")\nimage.width   # =\u003e 1024\nimage.height  # =\u003e 768\npixel = image[100, 200]\npixel.r  # =\u003e 182\npixel.g  # =\u003e 140\npixel.b  # =\u003e 97\n```\n\n### From binary data\n\n```ruby\nimage = PureJPEG.read(jpeg_bytes)\n```\n\n### Iterating pixels\n\n```ruby\nimage.each_pixel do |x, y, pixel|\n  puts \"#{x},#{y}: rgb(#{pixel.r}, #{pixel.g}, #{pixel.b})\"\nend\n```\n\n### Re-encoding\n\nA decoded `PureJPEG::Image` implements the same pixel source interface, so it can be passed directly back to the encoder:\n\n```ruby\nimage = PureJPEG.read(\"input.jpg\")\nPureJPEG.encode(image, quality: 60).write(\"recompressed.jpg\")\n```\n\n### Converting to PNG (with ChunkyPNG)\n\n```ruby\nimage = PureJPEG.read(\"photo.jpg\")\n\npng = ChunkyPNG::Image.new(image.width, image.height)\nimage.each_pixel do |x, y, pixel|\n  png[x, y] = ChunkyPNG::Color.rgb(pixel.r, pixel.g, pixel.b)\nend\npng.save(\"photo.png\")\n```\n\n## Format support\n\nEncoding:\n- Baseline DCT (SOF0)\n- 8-bit precision\n- Grayscale (1 component) and YCbCr color (3 components)\n- 4:2:0 chroma subsampling (color) or no subsampling (grayscale)\n- Standard Huffman tables (Annex K)\n\nDecoding:\n- Baseline DCT (SOF0) and Progressive DCT (SOF2)\n- 8-bit precision\n- 1-component (grayscale) and 3-component (YCbCr) images\n- Any chroma subsampling factor (4:4:4, 4:2:2, 4:2:0, etc.)\n- Restart markers (DRI/RST)\n\nNot supported: arithmetic coding, 12-bit precision, EXIF/ICC profile preservation, adding a default background for transparent sources (see what happens above!). Largely because I don't need these, but they are all do-able, especially with how loosely coupled this library is internally. Raise an issue if you really care about them!\n\n## Performance\n\nOn a 1024x1024 image (Ruby 4.0.1 on my M1 Max):\n\n| Operation | Time |\n|-----------|------|\n| Encode (color, q85) | ~1.7s |\n| Decode (color) | ~1.8s |\n\nBoth the encoder and decoder use a separable DCT with a precomputed cosine matrix and reuse all per-block buffers to minimize GC pressure. Pixel data is stored as packed integers internally to avoid per-pixel object allocation.\n\n## Some useful `rake` tasks\n\n```\nbundle install\nrake test        # run the test suite\nrake benchmark   # benchmark encoding (3 runs against examples/a.png)\nrake profile     # CPU profile with StackProf (requires the stackprof gem)\n```\n\n## AI Disclosure\n\n**Claude Code did the majority of the work.** The math of JPEG encoding/decoding is beyond me, except 'getting it' at a high level. I understand it like I understand the engine in my car :-)\n\n**I have read all of the code produced.** The algorithms are above my paygrade, but I'm OK with what has been produced, and I manually fixed a variety of stylistic things along the way. For example, CC seems to like wrapping entire functions in `if` statements rather than bailing on the opposite condition.\n\n**CC needed a lot of guidance.** Its initial JPEG algorithm was somewhat naive and output odd looking JPEGs akin to those of my Kodak digital camera from 2001. After some back and forth and image comparisons, we figured out it was doing the quantization entirely wrong (specifically not using the zigzag approach during quanitization but just going in raster order). I *like* this aesthetic, but fixed it up so that it works as a generally usable JPEG library, while adding ways to customize things so you can recreate the effect, if preferred (see `CREATIVE.md` for more on that).\n\n**CC is lazy.** The initial implementation was VERY SLOW. It took 15 seconds to turn a 1024x1024 PNG into a JPEG, so we went down the profiling rabbit hole and found many optimizations to make it ~6x faster. CC is poor at considering the role of Ruby's GC when implementing low level algorithms and needs some prodding to make the correct optimizations. CC is also lazy to the point of recommending that you just use another language (e.g. Go or Rust) rather than do a pure Ruby version of something - despite it being possible with some extra work.\n\n**CC's testing and cleanliness leaves a bit to be desired.** The CC-created tests were superficial, so I worked on getting them beefed up to tackle a variety of edge cases. They could still get better. It also didn't do RDoc comments, use Minitest, and a variety of other things I coerced it into working on. A good `CLAUDE.md` file could probably avoid many of these problems. I worked without one.\n\n**The overall experience was good.** I enjoyed this project, but CC clearly requires an experienced developer to keep it on the rails and to not end up with a bunch of buggy half-working crap. Getting to the basic 'turn a PNG into a JPEG' took only twenty minutes, but the rest of making it actually widely useful took several hours more.\n\n**The final 10% still takes 90% of the time.** As mentioned above, the first run was quick, but getting things right has taken much longer. v0.1-\u003e0.2 has taken longer than 0.1 did! But we now have progressive JPEG support, even more optimizations, better tests, etc. etc.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterc%2Fpure_jpeg","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpeterc%2Fpure_jpeg","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterc%2Fpure_jpeg/lists"}