Skip to content

Feature optional JPEG alpha channel #553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion lib/document.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PDFDocument extends stream.Readable
super

# PDF version
@version = 1.3
@version = 1.4

# Whether streams should be compressed
@compress = @options.compress ? yes
Expand Down
52 changes: 31 additions & 21 deletions lib/image.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,37 @@ Data = require './data'
JPEG = require './image/jpeg'
PNG = require './image/png'

toBuffer = (src) ->
if Buffer.isBuffer(src)
src
else if src instanceof ArrayBuffer
new Buffer(new Uint8Array(src))
else if match = /^data:.+;base64,(.*)$/.exec(src)
new Buffer(match[1], 'base64')
else
fs.readFileSync src

class PDFImage
@open: (src, label) ->
if Buffer.isBuffer(src)
data = src
else if src instanceof ArrayBuffer
data = new Buffer(new Uint8Array(src))
else
if match = /^data:.+;base64,(.*)$/.exec(src)
data = new Buffer(match[1], 'base64')

@open: (src, label, alphaSrc) ->
data = toBuffer src

if alphaSrc
alphaData = toBuffer alphaSrc
if not JPEG.is alphaData
throw Error 'Alpha mask must be a gray JPEG image'
alpha = new JPEG alphaData
if alpha.colorSpace != 'DeviceGray'
throw Error 'Alpha mask must be a gray JPEG image'

if data
if JPEG.is(data)
return new JPEG(data, label, alpha)

else if PNG.is(data)
return new PNG(data, label, alpha)

else
data = fs.readFileSync src
return unless data

if data[0] is 0xff and data[1] is 0xd8
return new JPEG(data, label)

else if data[0] is 0x89 and data.toString('ascii', 1, 4) is 'PNG'
return new PNG(data, label)

else
throw new Error 'Unknown image format.'

module.exports = PDFImage
throw new Error 'Unknown image format.'

module.exports = PDFImage
79 changes: 46 additions & 33 deletions lib/image/jpeg.coffee
Original file line number Diff line number Diff line change
@@ -1,41 +1,50 @@
fs = require 'fs'

class JPEG
MARKERS = [0xFFC0, 0xFFC1, 0xFFC2, 0xFFC3, 0xFFC5, 0xFFC6, 0xFFC7,
0xFFC8, 0xFFC9, 0xFFCA, 0xFFCB, 0xFFCC, 0xFFCD, 0xFFCE, 0xFFCF]

constructor: (@data, @label) ->
if @data.readUInt16BE(0) isnt 0xFFD8
throw "SOI not found in JPEG"

pos = 2
while pos < @data.length
marker = @data.readUInt16BE(pos)
pos += 2
break if marker in MARKERS
pos += @data.readUInt16BE(pos)

throw "Invalid JPEG." unless marker in MARKERS
pos += 2
MARKERS = [0xFFC0, 0xFFC1, 0xFFC2, 0xFFC3, 0xFFC5, 0xFFC6, 0xFFC7,
0xFFC8, 0xFFC9, 0xFFCA, 0xFFCB, 0xFFCC, 0xFFCD, 0xFFCE, 0xFFCF]

@bits = @data[pos++]
@height = @data.readUInt16BE(pos)
pos += 2
getMetaInfo = (data, result = {}) ->
if data.readUInt16BE(0) isnt 0xFFD8
throw "SOI not found in JPEG"

@width = @data.readUInt16BE(pos)
pos = 2
while pos < data.length
marker = data.readUInt16BE(pos)
pos += 2
break if marker in MARKERS
pos += data.readUInt16BE(pos)

channels = @data[pos++]
@colorSpace = switch channels
when 1 then 'DeviceGray'
when 3 then 'DeviceRGB'
when 4 then 'DeviceCMYK'

throw "Invalid JPEG." unless marker in MARKERS
pos += 2

result.bits = data[pos++]
result.height = data.readUInt16BE(pos)
pos += 2

result.width = data.readUInt16BE(pos)
pos += 2

channels = data[pos++]
result.colorSpace = switch channels
when 1 then 'DeviceGray'
when 3 then 'DeviceRGB'
when 4 then 'DeviceCMYK'

result

class JPEG

# Returns true if the given data buffer has a JPEG signature
@is: (data) ->
data[0] is 0xff and data[1] is 0xd8

constructor: (@data, @label, @alpha) ->
getMetaInfo @data, @
@obj = null

embed: (document) ->
return if @obj

@obj = document.ref
Type: 'XObject'
Subtype: 'Image'
Expand All @@ -44,16 +53,20 @@ class JPEG
Height: @height
ColorSpace: @colorSpace
Filter: 'DCTDecode'

# add extra decode params for CMYK images. By swapping the
# min and max values from the default, we invert the colors. See
# section 4.8.4 of the spec.
# section 4.8.4 of the spec.
if @colorSpace is 'DeviceCMYK'
@obj.data['Decode'] = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0]

if @alpha
@alpha.embed(document)
@obj.data['SMask'] = @alpha.obj

@obj.end @data

# free memory
@data = null

module.exports = JPEG
12 changes: 10 additions & 2 deletions lib/image/png.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ zlib = require 'zlib'
PNG = require 'png-js'

class PNGImage
constructor: (data, @label) ->

# Returns true if the given data buffer has a PNG signature
@is: (data) ->
data[0] is 0x89 and data.toString('ascii', 1, 4) is 'PNG'

constructor: (data, @label, @alpha) ->
@image = new PNG(data)
@width = @image.width
@height = @image.height
Expand Down Expand Up @@ -73,7 +78,10 @@ class PNGImage
@finalize()

finalize: ->
if @alphaChannel
if @alpha
@alpha.embed(@document)
@obj.data['SMask'] = @alpha.obj
else if @alphaChannel
sMask = @document.ref
Type: 'XObject'
Subtype: 'Image'
Expand Down
10 changes: 5 additions & 5 deletions lib/mixins/images.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports =
initImages: ->
@_imageRegistry = {}
@_imageCount = 0

image: (src, x, y, options = {}) ->
if typeof x is 'object'
options = x
Expand All @@ -20,7 +20,7 @@ module.exports =
if src.width and src.height
image = src
else
image = @openImage src
image = @openImage src, options.alpha

unless image.obj
image.embed this
Expand Down Expand Up @@ -77,7 +77,7 @@ module.exports =
else if options.valign is 'bottom'
y = y + bh - h

# Set the current y position to below the image if it is in the document flow
# Set the current y position to below the image if it is in the document flow
@y += h if @y is y

@save()
Expand All @@ -87,12 +87,12 @@ module.exports =

return this

openImage: (src) ->
openImage: (src, alpha) ->
if typeof src is 'string'
image = @_imageRegistry[src]

if not image
image = PDFImage.open src, 'I' + (++@_imageCount)
image = PDFImage.open src, 'I' + (++@_imageCount), alpha
if typeof src is 'string'
@_imageRegistry[src] = image

Expand Down